diff --git a/README.md b/README.md index 73f9c3e..920fc4f 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,18 @@ Support for other architectures can be enabled by adding the corresponding Maven ``` -**Supported platforms:** `Darwin`, `Windows`, `Linux`, `Alpine Linux` +**Supported platforms:** `Darwin`, `Windows`, `Linux`, `Alpine Linux`, `FreeBSD 13`, `FreeBSD 14` **Supported architectures:** `amd64`, `i386`, `arm32v6`, `arm32v7`, `arm64v8`, `ppc64le` Note that not all architectures are supported by all platforms, you can find an exhaustive list of all available artifacts here: https://mvnrepository.com/artifact/io.zonky.test.postgres +FreeBSD runtime compatibility verified so far: + +- `freebsd13` artifact on FreeBSD `13.5` +- `freebsd13` artifact on FreeBSD `14.4` +- `freebsd14` artifact on FreeBSD `14.4` +- `freebsd14` artifact on FreeBSD `15.0` + ## Building from Source The project uses a [Gradle](http://gradle.org)-based build system. In the instructions below, [`./gradlew`](http://vimeo.com/34436402) is invoked from the root of the source tree and serves as @@ -72,6 +79,8 @@ a cross-platform, self-contained bootstrap mechanism for the build. Be sure that your `JAVA_HOME` environment variable points to the `jdk1.6.0` folder extracted from the JDK download. +The Gradle wrapper used in this project is based on Gradle `6.9.3`. On modern machines it is safest to run the build with Java `8`, `11` or `17`. Java `19+` is not supported by this Gradle version. + Compiling non-native architectures rely on emulation, so it is necessary to register `qemu-*-static` executables: `docker run --rm --privileged multiarch/qemu-user-static:register --reset` @@ -101,6 +110,52 @@ Builds only a single binary for a specified platform and architecture. `./gradlew clean install -Pversion=18.3.0 -PpgVersion=18.3 -ParchName=arm64v8 -PdistName=alpine` +For the FreeBSD 13 amd64 artifact: + +`./gradlew clean :custom-freebsd-platform:install -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd13` + +For the FreeBSD 14 amd64 artifact: + +`./gradlew clean :custom-freebsd-platform:install -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd14` + +These builds run a FreeBSD guest inside QEMU from a Docker container. By default: + +- `freebsd13` uses the official FreeBSD `13.5-RELEASE` installer image +- `freebsd14` uses the official FreeBSD `14.4-RELEASE` installer image + +Downloads are cached under `.cache/freebsd-builder`. + +To override the FreeBSD installer image or cache directory: + +`./gradlew clean :custom-freebsd-platform:install -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd13 -PfreebsdImageUrl=https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/13.5/FreeBSD-13.5-RELEASE-amd64-disc1.iso.xz -PcacheDir=$PWD/.cache/freebsd-builder` + +`./gradlew clean :custom-freebsd-platform:install -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd14 -PfreebsdImageUrl=https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.4/FreeBSD-14.4-RELEASE-amd64-disc1.iso.xz -PcacheDir=$PWD/.cache/freebsd-builder` + +The generated runtime archive is named `postgres-freebsd13-x86_64.txz` or `postgres-freebsd14-x86_64.txz`, and the resulting jar artifact is named `embedded-postgres-binaries-freebsd13-amd64-.jar` or `embedded-postgres-binaries-freebsd14-amd64-.jar`. + +Current FreeBSD runtime assumption: + +- the bundled PostgreSQL binaries and shared libraries are packaged inside the artifact +- ICU data is expected to be available using the standard FreeBSD layout under `/usr/local/share/icu` + +In other words, the current FreeBSD artifact is suitable for embedded PostgreSQL on a normal FreeBSD host, but it is not yet a fully self-contained ICU runtime. + +### Test the FreeBSD artifact + +After creating the jar, the FreeBSD smoke test can be run with: + +`./gradlew :custom-freebsd-platform:test -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd13` + +`./gradlew :custom-freebsd-platform:test -Pversion=18.3.0 -PpgVersion=18.3 -PdistName=freebsd14` + +This smoke test boots a disposable FreeBSD guest for the requested major version and verifies: + +- `initdb` +- `pg_ctl` +- `SHOW SERVER_VERSION` +- `pgcrypto` +- `uuid-ossp` + It is also possible to include the PostGIS extension by passing the `postgisVersion` parameter, e.g. `-PpostgisVersion=2.5.2`. Note that this option is not (yet) available for Windows and Mac OS platforms. Optional parameters: @@ -112,7 +167,7 @@ Optional parameters: - supported values: `amd64`, `i386`, `arm32v6`, `arm32v7`, `arm64v8`, `ppc64le` - *distName* - default value: debian-like distribution - - supported values: the default value or `alpine` + - supported values: the default value, `alpine`, `freebsd13` or `freebsd14` - *dockerImage* - default value: resolved based on the platform - supported values: any supported docker image diff --git a/build.gradle b/build.gradle index 0b0897f..34f5386 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ ext { archNameParam = project.findProperty('archName') ?: '' distNameParam = project.findProperty('distName') ?: '' dockerImageParam = project.findProperty('dockerImage') ?: '' + freebsdImageUrlParam = project.findProperty('freebsdImageUrl') ?: '' + cacheDirParam = project.findProperty('cacheDir') ?: '' qemuPathParam = project.findProperty('qemuPath') ?: '' pgMajorVersionParam = (pgVersionParam =~ /(\d+).+/).with { matches() ? it[0][1].toInteger() : null } @@ -48,12 +50,23 @@ task validateInputs { if (!project.version || project.version == 'unspecified') { throw new GradleException("The 'version' property must be set") } - if (distNameParam && distNameParam != 'alpine') { - throw new GradleException("Currently only the 'alpine' distribution is supported") + if (distNameParam && !(distNameParam in ['alpine', 'freebsd13', 'freebsd14'])) { + throw new GradleException("Currently only the 'alpine', 'freebsd13' and 'freebsd14' distributions are supported") } if (archNameParam && !(archNameParam ==~ /^[a-z0-9]+$/)) { throw new GradleException("The 'archName' property must contain only alphanumeric characters") } + if ((distNameParam in ['freebsd13', 'freebsd14']) && archNameParam && archNameParam != 'amd64') { + throw new GradleException("Currently only the 'amd64' architecture is supported for FreeBSD distributions") + } + + if (distNameParam in ['freebsd13', 'freebsd14'] && freebsdImageUrlParam) { + def expectedMajor = extractFreebsdMajorFromDistName(distNameParam) + def actualMajor = extractFreebsdMajorFromImageUrl(freebsdImageUrlParam) + if (actualMajor != null && actualMajor != expectedMajor) { + throw new GradleException("The configured FreeBSD image URL targets major version ${actualMajor}, but distName=${distNameParam} expects FreeBSD ${expectedMajor}") + } + } } } @@ -419,6 +432,78 @@ alpineVariants.each { variant -> } } +project(':custom-freebsd-platform') { + if (distNameParam in ['freebsd13', 'freebsd14']) { + def archName = archNameParam ?: 'amd64' + def freebsdDistName = distNameParam + def freebsdMajor = extractFreebsdMajorFromDistName(freebsdDistName) + + task buildCustomFreebsdBundle(group: 'build (custom)', type: LazyExec, dependsOn: validateInputs) { + def dockerImage = { dockerImageParam ?: defaultFreebsdBuilderImage() } + def freebsdImageUrl = { freebsdImageUrlParam ?: defaultFreebsdImageUrl(freebsdMajor) } + def cacheDir = { cacheDirParam ?: "${rootDir}/.cache/freebsd-builder" } + + doFirst { + println "archName: $archName" + println "distName: $freebsdDistName" + println "dockerImage: ${dockerImage()}" + println "freebsdImageUrl: ${freebsdImageUrl()}" + println "cacheDir: ${cacheDir()}" + println '' + } + + inputs.property('pgVersion', pgVersionParam) + inputs.property('archName', archName) + inputs.property('dockerImage', dockerImage) + inputs.property('freebsdImageUrl', freebsdImageUrl) + inputs.property('cacheDir', cacheDir) + + inputs.file("$rootDir/scripts/build-postgres-freebsd.sh") + inputs.file("$rootDir/scripts/build-postgres-freebsd-guest.sh") + outputs.dir("$temporaryDir/bundle") + + workingDir temporaryDir + commandLine 'sh', "$rootDir/scripts/build-postgres-freebsd.sh", + '-v', "$pgVersionParam", + '-i', dockerImage, + '-u', freebsdImageUrl, + '-k', cacheDir + } + + task customFreebsdJar(group: 'build (custom)', type: Jar) { + from buildCustomFreebsdBundle + include "postgres-${freebsdDistName}-${normalizeArchName(archName)}.txz" + appendix = "${freebsdDistName}-${archName}" + } + + task testCustomFreebsdJar(group: 'build (custom)', type: LazyExec, dependsOn: [validateInputs, customFreebsdJar]) { + def dockerImage = { dockerImageParam ?: defaultFreebsdBuilderImage() } + def freebsdImageUrl = { freebsdImageUrlParam ?: defaultFreebsdImageUrl(freebsdMajor) } + def cacheDir = { cacheDirParam ?: "${rootDir}/.cache/freebsd-builder" } + + inputs.property('pgVersion', pgVersionParam) + inputs.property('archName', archName) + inputs.property('dockerImage', dockerImage) + inputs.property('freebsdImageUrl', freebsdImageUrl) + inputs.property('cacheDir', cacheDir) + + inputs.file("$rootDir/scripts/test-postgres-freebsd.sh") + inputs.file("$rootDir/scripts/test-postgres-freebsd-guest.sh") + + workingDir customFreebsdJar.destinationDirectory + commandLine 'sh', "$rootDir/scripts/test-postgres-freebsd.sh", + '-j', "embedded-postgres-binaries-${freebsdDistName}-${archName}-${version}.jar", + '-z', "postgres-${freebsdDistName}-${normalizeArchName(archName)}.txz", + '-i', dockerImage, + '-u', freebsdImageUrl, + '-v', "$pgVersionParam", + '-k', cacheDir + } + + artifacts.add('bundles', tasks.getByName('customFreebsdJar')) + } +} + subprojects { task sourcesJar(type: Jar, dependsOn: classes) { from sourceSets.main.allSource @@ -613,6 +698,39 @@ static def defaultAlpineImage(archName, useEmulation) { } } +static def defaultFreebsdBuilderImage() { + return 'ubuntu:24.04' +} + +static def defaultFreebsdImageUrl(int majorVersion) { + switch (majorVersion) { + case 13: + return defaultFreebsd13ImageUrl() + case 14: + return defaultFreebsd14ImageUrl() + default: + throw new GradleException("No default FreeBSD image URL is defined for major version ${majorVersion}") + } +} + +static def defaultFreebsd13ImageUrl() { + return 'https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/13.5/FreeBSD-13.5-RELEASE-amd64-disc1.iso.xz' +} + +static def defaultFreebsd14ImageUrl() { + return 'https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.4/FreeBSD-14.4-RELEASE-amd64-disc1.iso.xz' +} + +static def extractFreebsdMajorFromDistName(String distName) { + def matcher = (distName ?: '') =~ /^freebsd(\d+)$/ + return matcher.matches() ? matcher[0][1].toInteger() : null +} + +static def extractFreebsdMajorFromImageUrl(String imageUrl) { + def matcher = (imageUrl ?: '') =~ /(?:^|[^0-9])(1[0-9])(?:\.[0-9]+)?-RELEASE/ + return matcher.find() ? matcher.group(1).toInteger() : null +} + static def normalizeArchName(String input) { String arch = input.toLowerCase(Locale.US).replaceAll('[^a-z0-9]+', '') diff --git a/scripts/build-postgres-freebsd-guest.sh b/scripts/build-postgres-freebsd-guest.sh new file mode 100755 index 0000000..dfaea73 --- /dev/null +++ b/scripts/build-postgres-freebsd-guest.sh @@ -0,0 +1,263 @@ +#!/bin/sh +set -eu + +if [ -z "${PG_VERSION:-}" ]; then + echo "PG_VERSION environment variable is required!" >&2 + exit 1 +fi +if [ -n "${POSTGIS_VERSION:-}" ]; then + echo "PostGIS is not supported yet for the FreeBSD builder!" >&2 + exit 1 +fi + +PATH="/usr/local/bin:/usr/local/sbin:${PATH}" +DIST_DIR="${DIST_DIR:-/tmp/pg-dist}" +BUILD_ROOT="$(mktemp -d /tmp/postgresql-freebsd-build.XXXXXX)" +NCPU="$(sysctl -n hw.ncpu 2>/dev/null || echo 1)" +ICU_ENABLED=false +PG_SOURCE_FILE="${PG_SOURCE_FILE:-}" +FREEBSD_VERSION="$(freebsd-version -u 2>/dev/null || freebsd-version)" +FREEBSD_MAJOR="$(printf "%s" "$FREEBSD_VERSION" | sed "s/\..*//")" +ARCH_NAME="$(uname -m)" +CONFIGURE_LDFLAGS="-L/usr/local/lib -Wl,-z,origin" + +case "$ARCH_NAME" in + amd64) NORM_ARCH_NAME="x86_64" ;; + aarch64) NORM_ARCH_NAME="arm_64" ;; + i386) NORM_ARCH_NAME="x86_32" ;; + *) NORM_ARCH_NAME="$ARCH_NAME" ;; +esac + +cleanup() { + rm -rf "$BUILD_ROOT" +} +trap cleanup EXIT INT TERM + +copy_runtime_file() { + source_path="$1" + target_dir="$2" + target_path="$target_dir/$(basename "$source_path")" + resolved_path= + resolved_target_path= + + if [ -e "$target_path" ] || [ -L "$target_path" ]; then + : + elif [ -L "$source_path" ]; then + cp -pP "$source_path" "$target_path" + else + cp -p "$source_path" "$target_path" + fi + + if resolved_path=$(realpath "$source_path" 2>/dev/null); then + resolved_target_path="$target_dir/$(basename "$resolved_path")" + if [ "$resolved_path" != "$source_path" ] && [ ! -e "$resolved_target_path" ] && [ ! -L "$resolved_target_path" ]; then + cp -p "$resolved_path" "$resolved_target_path" + fi + fi +} + +should_bundle_system_lib() { + dep_path="$1" + + case "$dep_path" in + /usr/lib/libssl.so.*|/lib/libcrypto.so.*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +case "$PG_VERSION" in + 9.*) ICU_ENABLED=false ;; + *) ICU_ENABLED=true ;; +esac + +mkdir -p "$DIST_DIR" +env ASSUME_ALWAYS_YES=yes pkg bootstrap +env ASSUME_ALWAYS_YES=yes pkg update +env ASSUME_ALWAYS_YES=yes pkg install \ + ca_root_nss \ + curl \ + gmake \ + icu \ + libxml2 \ + libxslt \ + perl5 \ + pkgconf \ + python3 \ + tcl86 \ + bison \ + flex \ + patchelf + +if [ -n "$PG_SOURCE_FILE" ] && [ -f "$PG_SOURCE_FILE" ]; then + cp "$PG_SOURCE_FILE" "$BUILD_ROOT/postgresql.tar.bz2" +else + fetch -o "$BUILD_ROOT/postgresql.tar.bz2" "https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.bz2" +fi +mkdir -p "$BUILD_ROOT/postgresql" +tar -xjf "$BUILD_ROOT/postgresql.tar.bz2" -C "$BUILD_ROOT/postgresql" --strip-components 1 + +cd "$BUILD_ROOT/postgresql" +env CPPFLAGS="-I/usr/local/include" LDFLAGS="$CONFIGURE_LDFLAGS" \ + ./configure \ + CFLAGS="-Os" \ + PYTHON=/usr/local/bin/python3 \ + --prefix=/usr/local/pg-build \ + --enable-integer-datetimes \ + --enable-thread-safety \ + --with-uuid=bsd \ + --with-includes=/usr/local/include \ + --with-libraries=/usr/local/lib \ + $( [ "$ICU_ENABLED" = true ] && echo "--with-icu" ) \ + --with-libxml \ + --with-libxslt \ + --with-openssl \ + --with-perl \ + --with-python \ + --with-tcl \ + --without-readline + +# Make the default FreeBSD rpath point at our packaged lib directory. +perl -0pi -e 's/^rpathdir = \$\(libdir\)$/rpathdir = \$\$ORIGIN\/..\/lib/m' src/Makefile.global + +# Shared modules live in lib/postgresql, so they need a different relative rpath. +find contrib src/pl -type f \( -name Makefile -o -name GNUmakefile \) | while IFS= read -r makefile_path; do + if grep -Eq '^(MODULES|MODULE_big)[[:space:]]*=' "$makefile_path" && + ! grep -Eq '^(PROGRAM|PROGRAMS)[[:space:]]*=' "$makefile_path"; then + printf '\n# Embedded Postgres FreeBSD packaging uses a bundle-relative rpath.\nrpathdir = $$ORIGIN/..\n' >> "$makefile_path" + fi +done + +gmake -j"$NCPU" world-bin +gmake install-world-bin +gmake -C contrib install + +STAGE_PREFIX="postgres-freebsd${FREEBSD_MAJOR}" +RUNTIME_ROOT="$BUILD_ROOT/runtime" +RUNTIME_LIB_DIR="$RUNTIME_ROOT/lib" +RUNTIME_PLUGIN_DIR="$RUNTIME_LIB_DIR/postgresql" +RUNTIME_BIN_DIR="$RUNTIME_ROOT/bin" +RUNTIME_SHARE_DIR="$RUNTIME_ROOT/share" +SEEN_DEPS="$BUILD_ROOT/seen-deps.txt" + +set -x + +mkdir -p "$RUNTIME_PLUGIN_DIR" "$RUNTIME_BIN_DIR" "$RUNTIME_SHARE_DIR" +cp -Rp /usr/local/pg-build/share/postgresql "$RUNTIME_SHARE_DIR/" +if [ "$ICU_ENABLED" = true ] && [ -d /usr/local/share/icu ]; then + cp -Rp /usr/local/share/icu "$RUNTIME_SHARE_DIR/" +fi +cp -p /usr/local/pg-build/bin/initdb /usr/local/pg-build/bin/pg_ctl /usr/local/pg-build/bin/postgres "$RUNTIME_BIN_DIR/" +cp -Rp /usr/local/pg-build/lib/postgresql "$RUNTIME_LIB_DIR/" +for libpq_file in /usr/local/pg-build/lib/libpq.so*; do + copy_runtime_file "$libpq_file" "$RUNTIME_LIB_DIR" +done + +collect_deps() { + object_path="$1" + + [ -e "$object_path" ] || return + if [ -f "$SEEN_DEPS" ] && grep -Fxq "$object_path" "$SEEN_DEPS"; then + return + fi + printf "%s\n" "$object_path" >> "$SEEN_DEPS" + + deps_file="$BUILD_ROOT/ldd.$(basename "$object_path").txt" + ldd "$object_path" 2>/dev/null | awk ' + /^[^[:space:]]+:$/ { next } + { + for (i = 1; i <= NF; i++) { + if ($i ~ /^\//) { + gsub(/:$/, "", $i) + print $i + } + } + } + ' > "$deps_file" + + while IFS= read -r dep_path; do + case "$dep_path" in + /usr/local/pg-build/*) + collect_deps "$dep_path" + ;; + /usr/local/*) + copy_runtime_file "$dep_path" "$RUNTIME_LIB_DIR" + collect_deps "$dep_path" + ;; + *) + if should_bundle_system_lib "$dep_path"; then + copy_runtime_file "$dep_path" "$RUNTIME_LIB_DIR" + collect_deps "$dep_path" + fi + ;; + esac + done < "$deps_file" +} + +collect_deps /usr/local/pg-build/bin/initdb +collect_deps /usr/local/pg-build/bin/pg_ctl +collect_deps /usr/local/pg-build/bin/postgres + +find /usr/local/pg-build/lib/postgresql -maxdepth 1 -type f -name "*.so" | while IFS= read -r module_path; do + collect_deps "$module_path" +done + +find "$RUNTIME_BIN_DIR" -type f \( -name "initdb" -o -name "pg_ctl" -o -name "postgres" \) -print0 | \ + xargs -0 -n1 patchelf --set-rpath '$ORIGIN/../lib' +find "$RUNTIME_LIB_DIR" -maxdepth 1 -type f -name "*.so*" -print0 | \ + xargs -0 -n1 patchelf --set-rpath '$ORIGIN' +find "$RUNTIME_PLUGIN_DIR" -maxdepth 1 -type f -name "*.so*" -print0 | \ + xargs -0 -n1 patchelf --set-rpath '$ORIGIN/..' + +find /usr/local/pg-build -type f | sort > "$DIST_DIR/${STAGE_PREFIX}-build-manifest.txt" +find "$RUNTIME_ROOT" -type f | sort > "$DIST_DIR/${STAGE_PREFIX}-runtime-manifest.txt" +pkg info > "$DIST_DIR/${STAGE_PREFIX}-pkg-info.txt" +{ + echo "# ldd: bin/initdb" + ldd /usr/local/pg-build/bin/initdb || true + echo + echo "# ldd: bin/pg_ctl" + ldd /usr/local/pg-build/bin/pg_ctl || true + echo + echo "# ldd: bin/postgres" + ldd /usr/local/pg-build/bin/postgres || true + echo + echo "# elfdump: bin/initdb" + elfdump -d /usr/local/pg-build/bin/initdb || true + echo + echo "# elfdump: bin/pg_ctl" + elfdump -d /usr/local/pg-build/bin/pg_ctl || true + echo + echo "# elfdump: bin/postgres" + elfdump -d /usr/local/pg-build/bin/postgres || true + echo + echo "# elfdump: runtime/bin/initdb" + elfdump -d "$RUNTIME_BIN_DIR/initdb" || true + echo + echo "# elfdump: runtime/bin/pg_ctl" + elfdump -d "$RUNTIME_BIN_DIR/pg_ctl" || true + echo + echo "# elfdump: runtime/bin/postgres" + elfdump -d "$RUNTIME_BIN_DIR/postgres" || true + echo + echo "# elfdump: runtime/lib/libicui18n.so.76" + elfdump -d "$RUNTIME_LIB_DIR/libicui18n.so.76" || true + echo + echo "# elfdump: runtime/lib/libicudata.so.76" + elfdump -d "$RUNTIME_LIB_DIR/libicudata.so.76" || true +} > "$DIST_DIR/${STAGE_PREFIX}-ldd.txt" + +tar -C /usr/local -cJf "$DIST_DIR/${STAGE_PREFIX}-build.txz" pg-build +tar -C "$RUNTIME_ROOT" -cJf "$DIST_DIR/${STAGE_PREFIX}-${NORM_ARCH_NAME}.txz" \ + share/postgresql \ + $( [ -d "$RUNTIME_SHARE_DIR/icu" ] && echo share/icu ) \ + lib \ + bin/initdb \ + bin/pg_ctl \ + bin/postgres + +echo "Generated FreeBSD build artifacts:" +find "$DIST_DIR" -maxdepth 1 -type f | sort diff --git a/scripts/build-postgres-freebsd.sh b/scripts/build-postgres-freebsd.sh new file mode 100755 index 0000000..6b40740 --- /dev/null +++ b/scripts/build-postgres-freebsd.sh @@ -0,0 +1,359 @@ +#!/bin/bash +set -euo pipefail + +DOCKER_OPTS= +POSTGIS_VERSION= +VM_IMAGE_URL= +VM_MEMORY_MB=4096 +VM_CPUS=4 +VM_DISK_SIZE=12G +SSH_PORT=2222 +SERIAL_PORT=4444 +CACHE_DIR= + +while getopts "v:i:g:o:u:m:c:p:s:k:r:" opt; do + case $opt in + v) PG_VERSION=$OPTARG ;; + i) IMG_NAME=$OPTARG ;; + g) POSTGIS_VERSION=$OPTARG ;; + o) DOCKER_OPTS=$OPTARG ;; + u) VM_IMAGE_URL=$OPTARG ;; + m) VM_MEMORY_MB=$OPTARG ;; + c) VM_CPUS=$OPTARG ;; + p) SSH_PORT=$OPTARG ;; + s) VM_DISK_SIZE=$OPTARG ;; + k) CACHE_DIR=$OPTARG ;; + r) SERIAL_PORT=$OPTARG ;; + \?) exit 1 ;; + esac +done + +if [ -z "${PG_VERSION:-}" ] ; then + echo "Postgres version parameter is required!" && exit 1; +fi +if [ -z "${IMG_NAME:-}" ] ; then + echo "Docker image parameter is required!" && exit 1; +fi +if [ -z "${VM_IMAGE_URL:-}" ] ; then + echo "FreeBSD VM image URL parameter is required!" && exit 1; +fi +if [ -n "${POSTGIS_VERSION:-}" ] ; then + echo "PostGIS is not supported yet for the FreeBSD builder!" && exit 1; +fi + +PROVISION_MODE= +if echo "$VM_IMAGE_URL" | grep -q 'BASIC-CLOUDINIT'; then + PROVISION_MODE=cloudinit +elif echo "$VM_IMAGE_URL" | grep -Eq '(disc1|dvd1)\.iso(\.xz)?$'; then + PROVISION_MODE=installer +else + echo "FreeBSD VM image URL must point to either a BASIC-CLOUDINIT image or a release disc1.iso/dvd1.iso installer image." && exit 1; +fi + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +TRG_DIR=$PWD/bundle +CACHE_DIR=${CACHE_DIR:-$PWD/.cache/freebsd-builder} +mkdir -p "$TRG_DIR" +mkdir -p "$CACHE_DIR" +rm -f "$TRG_DIR"/postgres-freebsd*.txz "$TRG_DIR"/postgres-freebsd*.txt 2>/dev/null || true + +docker run -i --rm \ + -v "${TRG_DIR}:/usr/local/pg-dist" \ + -v "${CACHE_DIR}:/usr/local/pg-cache" \ + -v "${SCRIPT_DIR}:/usr/local/pg-scripts:ro" \ + -e IMG_NAME="$IMG_NAME" \ + -e PG_VERSION="$PG_VERSION" \ + -e POSTGIS_VERSION="$POSTGIS_VERSION" \ + -e PROVISION_MODE="$PROVISION_MODE" \ + -e VM_IMAGE_URL="$VM_IMAGE_URL" \ + -e VM_MEMORY_MB="$VM_MEMORY_MB" \ + -e VM_CPUS="$VM_CPUS" \ + -e VM_DISK_SIZE="$VM_DISK_SIZE" \ + -e SSH_PORT="$SSH_PORT" \ + -e SERIAL_PORT="$SERIAL_PORT" \ + $DOCKER_OPTS "$IMG_NAME" /bin/bash -eu -c ' + log() { + echo "[freebsd-builder] $*" + } + + wait_for_ssh() { + local user=$1 + local max_attempts=$2 + + log "Waiting for SSH from FreeBSD guest as ${user}" + for attempt in $(seq 1 "$max_attempts"); do + if ssh $SSH_OPTS "${user}@127.0.0.1" true >/dev/null 2>&1; then + log "SSH is reachable as ${user}" + return 0 + fi + if [ $((attempt % 6)) -eq 0 ]; then + log "SSH not ready yet after $((attempt * 5))s" + tail -n 20 "$WORK_DIR/serial.log" || true + fi + sleep 5 + done + + echo "FreeBSD guest did not become reachable over SSH as ${user}" >&2 + tail -n 200 "$WORK_DIR/serial.log" >&2 || true + exit 1 + } + + run_guest_build() { + local user=$1 + local runner=$2 + + if [ -f "/usr/local/pg-cache/postgresql-${PG_VERSION}.tar.bz2" ]; then + log "Copying cached PostgreSQL source tarball into FreeBSD guest" + scp $SCP_OPTS "/usr/local/pg-cache/postgresql-${PG_VERSION}.tar.bz2" "${user}@127.0.0.1:/tmp/postgresql-${PG_VERSION}.tar.bz2" + fi + + log "Copying guest build helper" + scp $SCP_OPTS /usr/local/pg-scripts/build-postgres-freebsd-guest.sh "${user}@127.0.0.1:/tmp/build-postgres-freebsd-guest.sh" + + log "Building PostgreSQL $PG_VERSION inside FreeBSD guest" + if [ -n "$runner" ]; then + ssh $SSH_OPTS "${user}@127.0.0.1" "chmod +x /tmp/build-postgres-freebsd-guest.sh && ${runner} env PG_VERSION=$PG_VERSION POSTGIS_VERSION=$POSTGIS_VERSION DIST_DIR=/tmp/pg-dist PG_SOURCE_FILE=/tmp/postgresql-${PG_VERSION}.tar.bz2 /tmp/build-postgres-freebsd-guest.sh" + else + ssh $SSH_OPTS "${user}@127.0.0.1" "chmod +x /tmp/build-postgres-freebsd-guest.sh && env PG_VERSION=$PG_VERSION POSTGIS_VERSION=$POSTGIS_VERSION DIST_DIR=/tmp/pg-dist PG_SOURCE_FILE=/tmp/postgresql-${PG_VERSION}.tar.bz2 /tmp/build-postgres-freebsd-guest.sh" + fi + + log "Files produced inside FreeBSD guest:" + ssh $SSH_OPTS "${user}@127.0.0.1" "find /tmp/pg-dist -maxdepth 1 -type f | sort" + log "Copying staged FreeBSD artifacts back to host" + ssh $SSH_OPTS "${user}@127.0.0.1" "tar -C /tmp/pg-dist -cf - ." | tar -C /usr/local/pg-dist -xf - + log "Files copied back to host:" + find /usr/local/pg-dist -maxdepth 1 -type f | sort + } + + export DEBIAN_FRONTEND=noninteractive + log "Installing host-side dependencies inside Docker image $IMG_NAME" + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + expect \ + netcat-openbsd \ + openssh-client \ + qemu-system-x86 \ + qemu-utils \ + xorriso \ + xz-utils + + WORK_DIR=$(mktemp -d /tmp/freebsd-builder.XXXXXX) + cleanup() { + local errcode=$? + if [ -f "$WORK_DIR/qemu.pid" ]; then + kill "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + fi + if [ -f "$WORK_DIR/serial-driver.pid" ]; then + kill "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + fi + rm -rf "$WORK_DIR" + return $errcode + } + trap cleanup EXIT + + ssh-keygen -q -t ed25519 -N "" -f "$WORK_DIR/id_ed25519" + SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -p $SSH_PORT" + SCP_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -P $SSH_PORT" + + if [ ! -f "/usr/local/pg-cache/postgresql-${PG_VERSION}.tar.bz2" ]; then + log "Downloading PostgreSQL source archive for cache" + curl -fsSL "https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.bz2" -o "/usr/local/pg-cache/postgresql-${PG_VERSION}.tar.bz2" + else + log "Using cached PostgreSQL source archive" + fi + + if [ -e /dev/kvm ]; then + QEMU_ACCEL=kvm + QEMU_CPU=max + else + QEMU_ACCEL=tcg + QEMU_CPU=qemu64 + fi + + if [ "$PROVISION_MODE" = cloudinit ]; then + mkdir -p "$WORK_DIR/seed" + VM_CACHE_BASENAME=$(basename "$VM_IMAGE_URL") + if [ ! -f "/usr/local/pg-cache/${VM_CACHE_BASENAME}" ]; then + log "Downloading FreeBSD cloud image from $VM_IMAGE_URL" + curl -fsSL "$VM_IMAGE_URL" -o "/usr/local/pg-cache/${VM_CACHE_BASENAME}" + else + log "Using cached FreeBSD cloud image ${VM_CACHE_BASENAME}" + fi + if echo "$VM_IMAGE_URL" | grep -q "\.xz$"; then + log "Decompressing FreeBSD cloud image" + xz -dc "/usr/local/pg-cache/${VM_CACHE_BASENAME}" > "$WORK_DIR/freebsd-base.qcow2" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-base.qcow2" + fi + + log "Creating writable overlay" + qemu-img create -q -f qcow2 -F qcow2 -b "$WORK_DIR/freebsd-base.qcow2" "$WORK_DIR/freebsd-overlay.qcow2" + + cat > "$WORK_DIR/seed/meta-data" < "$WORK_DIR/seed/user-data" < "$WORK_DIR/freebsd-installer.iso" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-installer.iso" + fi + + cat > "$WORK_DIR/install-media/etc/installerconfig" < /root/.ssh/authorized_keys <<'"'"'KEYEOF'"'"' +$(cat "$WORK_DIR/id_ed25519.pub") +KEYEOF +chmod 600 /root/.ssh/authorized_keys +cat > /boot.config <<'"'"'BOOTEOF'"'"' +-Dh +BOOTEOF +cat > /boot/loader.conf <<'"'"'LOADERCONFEOF'"'"' +console="comconsole,vidconsole" +boot_multicons="YES" +autoboot_delay="1" +LOADERCONFEOF +printf "\nPermitRootLogin yes\nPasswordAuthentication no\nChallengeResponseAuthentication no\n" >> /etc/ssh/sshd_config +EOF + + cat > "$WORK_DIR/install-media/boot.config" < "$WORK_DIR/install-media/boot/loader.conf" < "$WORK_DIR/drive-serial.expect" < "$WORK_DIR/serial-driver.pid" + + wait_for_ssh root 360 + run_guest_build root "" + fi + + log "FreeBSD build flow completed" + ' diff --git a/scripts/test-embedded-postgres-freebsd-guest.sh b/scripts/test-embedded-postgres-freebsd-guest.sh new file mode 100755 index 0000000..d0dcce5 --- /dev/null +++ b/scripts/test-embedded-postgres-freebsd-guest.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -eu + +if [ -z "${BUNDLE_FILE:-}" ]; then + echo "BUNDLE_FILE environment variable is required!" >&2 + exit 1 +fi +if [ -z "${REPO_DIR:-}" ]; then + echo "REPO_DIR environment variable is required!" >&2 + exit 1 +fi + +env ASSUME_ALWAYS_YES=yes pkg bootstrap +env ASSUME_ALWAYS_YES=yes pkg update +env ASSUME_ALWAYS_YES=yes pkg install \ + ca_root_nss \ + go \ + git + +BINARIES_PATH=/var/tmp/embedded-postgres-binaries +RUNTIME_PATH=/var/tmp/embedded-postgres-runtime +DATA_PATH=/var/tmp/embedded-postgres-data + +rm -rf "$BINARIES_PATH" "$RUNTIME_PATH" "$DATA_PATH" +mkdir -p "$BINARIES_PATH" /usr/local/share +tar -xJf "$BUNDLE_FILE" -C "$BINARIES_PATH" + +if [ -d "$BINARIES_PATH/share/icu" ]; then + rm -rf /usr/local/share/icu + cp -Rp "$BINARIES_PATH/share/icu" /usr/local/share/ +fi + +cd "$REPO_DIR/examples" +env \ + EMBEDDED_POSTGRES_BINARIES_PATH="$BINARIES_PATH" \ + EMBEDDED_POSTGRES_RUNTIME_PATH="$RUNTIME_PATH" \ + EMBEDDED_POSTGRES_DATA_PATH="$DATA_PATH" \ + EMBEDDED_POSTGRES_PORT=65432 \ + go test ./... \ + -count=1 \ + -v diff --git a/scripts/test-embedded-postgres-freebsd.sh b/scripts/test-embedded-postgres-freebsd.sh new file mode 100755 index 0000000..32b96f5 --- /dev/null +++ b/scripts/test-embedded-postgres-freebsd.sh @@ -0,0 +1,354 @@ +#!/bin/bash +set -euo pipefail + +DOCKER_OPTS= +VM_IMAGE_URL= +VM_MEMORY_MB=4096 +VM_CPUS=4 +VM_DISK_SIZE=16G +SSH_PORT=2222 +SERIAL_PORT=4444 +CACHE_DIR= +REPO_DIR= +BUNDLE_FILE= + +while getopts "i:u:m:c:p:s:k:r:b:o:" opt; do + case $opt in + i) IMG_NAME=$OPTARG ;; + u) VM_IMAGE_URL=$OPTARG ;; + m) VM_MEMORY_MB=$OPTARG ;; + c) VM_CPUS=$OPTARG ;; + p) SSH_PORT=$OPTARG ;; + s) VM_DISK_SIZE=$OPTARG ;; + k) CACHE_DIR=$OPTARG ;; + r) REPO_DIR=$OPTARG ;; + b) BUNDLE_FILE=$OPTARG ;; + o) DOCKER_OPTS=$OPTARG ;; + \?) exit 1 ;; + esac +done + +if [ -z "${IMG_NAME:-}" ] ; then + echo "Docker image parameter is required!" && exit 1; +fi +if [ -z "${VM_IMAGE_URL:-}" ] ; then + echo "FreeBSD VM image URL parameter is required!" && exit 1; +fi +if [ -z "${REPO_DIR:-}" ] ; then + echo "embedded-postgres repository path parameter is required!" && exit 1; +fi +if [ -z "${BUNDLE_FILE:-}" ] ; then + echo "FreeBSD postgres bundle parameter is required!" && exit 1; +fi + +PROVISION_MODE= +if echo "$VM_IMAGE_URL" | grep -q 'BASIC-CLOUDINIT'; then + PROVISION_MODE=cloudinit +elif echo "$VM_IMAGE_URL" | grep -Eq '(disc1|dvd1)\.iso(\.xz)?$'; then + PROVISION_MODE=installer +else + echo "FreeBSD VM image URL must point to either a BASIC-CLOUDINIT image or a release disc1.iso/dvd1.iso installer image." && exit 1; +fi + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +LIB_DIR=$PWD +CACHE_DIR=${CACHE_DIR:-$PWD/.cache/freebsd-builder} +mkdir -p "$CACHE_DIR" + +if [ ! -d "$REPO_DIR" ] ; then + echo "embedded-postgres repository not found: $REPO_DIR" && exit 1; +fi +if [ ! -f "$BUNDLE_FILE" ] ; then + if [ ! -f "$LIB_DIR/$BUNDLE_FILE" ] ; then + echo "Bundle file not found: $BUNDLE_FILE" && exit 1; + fi + BUNDLE_HOST_PATH="$LIB_DIR/$BUNDLE_FILE" +else + BUNDLE_HOST_PATH="$BUNDLE_FILE" +fi + +REPO_HOST_PATH=$(cd "$REPO_DIR" && pwd) +BUNDLE_HOST_DIR=$(cd "$(dirname "$BUNDLE_HOST_PATH")" && pwd) +BUNDLE_HOST_BASENAME=$(basename "$BUNDLE_HOST_PATH") + +docker run -i --rm \ + -v "${REPO_HOST_PATH}:/usr/local/src/embedded-postgres:ro" \ + -v "${BUNDLE_HOST_DIR}:/usr/local/pg-bundle:ro" \ + -v "${CACHE_DIR}:/usr/local/pg-cache" \ + -v "${SCRIPT_DIR}:/usr/local/pg-scripts:ro" \ + -e IMG_NAME="$IMG_NAME" \ + -e VM_IMAGE_URL="$VM_IMAGE_URL" \ + -e VM_MEMORY_MB="$VM_MEMORY_MB" \ + -e VM_CPUS="$VM_CPUS" \ + -e VM_DISK_SIZE="$VM_DISK_SIZE" \ + -e PROVISION_MODE="$PROVISION_MODE" \ + -e SSH_PORT="$SSH_PORT" \ + -e SERIAL_PORT="$SERIAL_PORT" \ + -e BUNDLE_HOST_BASENAME="$BUNDLE_HOST_BASENAME" \ + $DOCKER_OPTS "$IMG_NAME" /bin/bash -eu -c ' + log() { + echo "[embedded-postgres-freebsd-test] $*" + } + + wait_for_ssh() { + local user=$1 + local max_attempts=$2 + + log "Waiting for SSH from FreeBSD guest as ${user}" + for attempt in $(seq 1 "$max_attempts"); do + if ssh $SSH_OPTS "${user}@127.0.0.1" true >/dev/null 2>&1; then + log "SSH is reachable as ${user}" + return 0 + fi + if [ $((attempt % 6)) -eq 0 ]; then + log "SSH not ready yet after $((attempt * 5))s" + tail -n 20 "$WORK_DIR/serial.log" || true + fi + sleep 5 + done + + echo "FreeBSD guest did not become reachable over SSH as ${user}" >&2 + tail -n 200 "$WORK_DIR/serial.log" >&2 || true + exit 1 + } + + export DEBIAN_FRONTEND=noninteractive + log "Installing host-side dependencies inside Docker image $IMG_NAME" + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + expect \ + netcat-openbsd \ + openssh-client \ + qemu-system-x86 \ + qemu-utils \ + xorriso \ + xz-utils + + WORK_DIR=$(mktemp -d /tmp/embedded-postgres-freebsd-test.XXXXXX) + cleanup() { + local errcode=$? + if [ -f "$WORK_DIR/qemu.pid" ]; then + kill "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + fi + if [ -f "$WORK_DIR/serial-driver.pid" ]; then + kill "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + fi + rm -rf "$WORK_DIR" + return $errcode + } + trap cleanup EXIT + + ssh-keygen -q -t ed25519 -N "" -f "$WORK_DIR/id_ed25519" + SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -p $SSH_PORT" + SCP_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -P $SSH_PORT" + + if [ -e /dev/kvm ]; then + QEMU_ACCEL=kvm + QEMU_CPU=max + else + QEMU_ACCEL=tcg + QEMU_CPU=qemu64 + fi + + VM_CACHE_BASENAME=$(basename "$VM_IMAGE_URL") + if [ ! -f "/usr/local/pg-cache/${VM_CACHE_BASENAME}" ]; then + if [ "$PROVISION_MODE" = cloudinit ]; then + log "Downloading FreeBSD cloud image from $VM_IMAGE_URL" + else + log "Downloading FreeBSD installer ISO from $VM_IMAGE_URL" + fi + curl -fsSL "$VM_IMAGE_URL" -o "/usr/local/pg-cache/${VM_CACHE_BASENAME}" + else + if [ "$PROVISION_MODE" = cloudinit ]; then + log "Using cached FreeBSD cloud image ${VM_CACHE_BASENAME}" + else + log "Using cached FreeBSD installer ISO ${VM_CACHE_BASENAME}" + fi + fi + + if [ "$PROVISION_MODE" = cloudinit ]; then + mkdir -p "$WORK_DIR/seed" + if echo "$VM_IMAGE_URL" | grep -q "\.xz$"; then + log "Decompressing FreeBSD cloud image" + xz -dc "/usr/local/pg-cache/${VM_CACHE_BASENAME}" > "$WORK_DIR/freebsd-base.qcow2" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-base.qcow2" + fi + + log "Creating writable overlay" + qemu-img create -q -f qcow2 -F qcow2 -b "$WORK_DIR/freebsd-base.qcow2" "$WORK_DIR/freebsd-overlay.qcow2" + + cat > "$WORK_DIR/seed/meta-data" < "$WORK_DIR/seed/user-data" < "$WORK_DIR/freebsd-installer.iso" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-installer.iso" + fi + + cat > "$WORK_DIR/install-media/etc/installerconfig" < /root/.ssh/authorized_keys <<'"'"'KEYEOF'"'"' +$(cat "$WORK_DIR/id_ed25519.pub") +KEYEOF +chmod 600 /root/.ssh/authorized_keys +cat > /boot.config <<'"'"'BOOTEOF'"'"' +-Dh +BOOTEOF +cat > /boot/loader.conf <<'"'"'LOADERCONFEOF'"'"' +console="comconsole,vidconsole" +boot_multicons="YES" +autoboot_delay="1" +LOADERCONFEOF +printf "\nPermitRootLogin yes\nPasswordAuthentication no\nChallengeResponseAuthentication no\n" >> /etc/ssh/sshd_config +EOF + + cat > "$WORK_DIR/install-media/boot.config" < "$WORK_DIR/install-media/boot/loader.conf" < "$WORK_DIR/drive-serial.expect" < "$WORK_DIR/serial-driver.pid" + + wait_for_ssh root 360 + + log "Copying embedded-postgres repo, bundle and guest helper" + tar -C /usr/local/src -cf - embedded-postgres | ssh $SSH_OPTS root@127.0.0.1 "mkdir -p /var/tmp && tar -xf - -C /var/tmp" + scp $SCP_OPTS "/usr/local/pg-bundle/${BUNDLE_HOST_BASENAME}" root@127.0.0.1:/tmp/"${BUNDLE_HOST_BASENAME}" + scp $SCP_OPTS /usr/local/pg-scripts/test-embedded-postgres-freebsd-guest.sh root@127.0.0.1:/tmp/test-embedded-postgres-freebsd-guest.sh + + log "Running embedded-postgres examples inside FreeBSD guest" + ssh $SSH_OPTS root@127.0.0.1 "chmod +x /tmp/test-embedded-postgres-freebsd-guest.sh && env BUNDLE_FILE=/tmp/${BUNDLE_HOST_BASENAME} REPO_DIR=/var/tmp/embedded-postgres /tmp/test-embedded-postgres-freebsd-guest.sh" + fi + + log "embedded-postgres FreeBSD example test completed" + ' diff --git a/scripts/test-postgres-freebsd-guest.sh b/scripts/test-postgres-freebsd-guest.sh new file mode 100755 index 0000000..18b8f96 --- /dev/null +++ b/scripts/test-postgres-freebsd-guest.sh @@ -0,0 +1,79 @@ +#!/bin/sh +set -eu + +if [ -z "${JAR_FILE:-}" ]; then + echo "JAR_FILE environment variable is required!" >&2 + exit 1 +fi +if [ -z "${ZIP_FILE:-}" ]; then + echo "ZIP_FILE environment variable is required!" >&2 + exit 1 +fi +if [ -z "${PG_VERSION:-}" ]; then + echo "PG_VERSION environment variable is required!" >&2 + exit 1 +fi + +TEST_ROOT="$(mktemp -d /var/tmp/postgresql-freebsd-test.XXXXXX)" +TEST_USER="pgtest" +PG_MAJOR="$(printf "%s" "$PG_VERSION" | sed 's/\..*//')" +PG_ENV= + +cleanup() { + set +e + if [ -x "$TEST_ROOT/pg-test/bin/pg_ctl" ] && [ -d "$TEST_ROOT/pg-test/data" ]; then + su -m "$TEST_USER" -c "$TEST_ROOT/pg-test/bin/pg_ctl -w -D $TEST_ROOT/pg-test/data stop" >/dev/null 2>&1 || true + fi + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT INT TERM + +env ASSUME_ALWAYS_YES=yes pkg bootstrap +env ASSUME_ALWAYS_YES=yes pkg update +env ASSUME_ALWAYS_YES=yes pkg install \ + unzip \ + "postgresql${PG_MAJOR}-client" + +mkdir -p "$TEST_ROOT/pg-dist" +unzip -q -d "$TEST_ROOT/pg-dist" "$JAR_FILE" +mkdir -p "$TEST_ROOT/pg-test/data" +tar -xJf "$TEST_ROOT/pg-dist/$ZIP_FILE" -C "$TEST_ROOT/pg-test" +chmod 755 "$TEST_ROOT/pg-test/bin/initdb" "$TEST_ROOT/pg-test/bin/pg_ctl" "$TEST_ROOT/pg-test/bin/postgres" +ICU_DATA_DIR= +ICU_DATA_PATH= +if [ -d "$TEST_ROOT/pg-test/share/icu" ]; then + mkdir -p /usr/local/share + rm -rf /usr/local/share/icu + cp -Rp "$TEST_ROOT/pg-test/share/icu" /usr/local/share/ + ICU_DATA_DIR="$(find "$TEST_ROOT/pg-test/share/icu" -mindepth 1 -maxdepth 1 -type d | sort | head -n 1)" +fi +if [ -n "$ICU_DATA_DIR" ]; then + ICU_DATA_PATH="$(find "$ICU_DATA_DIR" -maxdepth 1 -type f -name 'icudt*.dat' | sort | head -n 1)" +fi +PG_ENV="env" + +echo "FreeBSD ICU debug:" >&2 +echo " ICU_DATA_DIR=${ICU_DATA_DIR:-}" >&2 +echo " ICU_DATA_PATH=${ICU_DATA_PATH:-}" >&2 +echo " PG_ENV=$PG_ENV" >&2 +if [ -d "$TEST_ROOT/pg-test/share/icu" ]; then + find "$TEST_ROOT/pg-test/share/icu" -maxdepth 2 \( -type d -o -type f \) | sort >&2 +fi +if [ -d /usr/local/share/icu ]; then + find /usr/local/share/icu -maxdepth 2 \( -type d -o -type f \) | sort >&2 +fi + +pw groupshow "$TEST_USER" >/dev/null 2>&1 || pw useradd "$TEST_USER" -m -s /bin/sh +chown -R "$TEST_USER:$TEST_USER" "$TEST_ROOT" + +su -m "$TEST_USER" -c "$PG_ENV $TEST_ROOT/pg-test/bin/initdb -A trust -U postgres -D $TEST_ROOT/pg-test/data -E UTF-8" +su -m "$TEST_USER" -c "$PG_ENV $TEST_ROOT/pg-test/bin/pg_ctl -w -D $TEST_ROOT/pg-test/data -o \"-p 65432 -F -c timezone=UTC -c synchronous_commit=off -c max_connections=300\" start" + +test "$(psql -qAtX -h localhost -p 65432 -U postgres -d postgres -c "SHOW SERVER_VERSION")" = "$PG_VERSION" +test "$(psql -qAtX -h localhost -p 65432 -U postgres -d postgres -c "CREATE EXTENSION pgcrypto; SELECT digest('test', 'sha256');")" = "\x9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" +echo "$(psql -qAtX -h localhost -p 65432 -U postgres -d postgres -c 'CREATE EXTENSION "uuid-ossp"; SELECT uuid_generate_v4();')" | grep -E '^[^-]{8}-[^-]{4}-[^-]{4}-[^-]{4}-[^-]{12}$' + +if echo "$PG_VERSION" | grep -qvE '^(10|9)\.' ; then + count="$(psql -qAtX -h localhost -p 65432 -U postgres -d postgres -c 'SET jit_above_cost = 10; SELECT SUM(relpages) FROM pg_class;')" + test "$count" -gt 0 +fi diff --git a/scripts/test-postgres-freebsd.sh b/scripts/test-postgres-freebsd.sh new file mode 100755 index 0000000..28145c9 --- /dev/null +++ b/scripts/test-postgres-freebsd.sh @@ -0,0 +1,354 @@ +#!/bin/bash +set -euo pipefail + +DOCKER_OPTS= +VM_IMAGE_URL= +VM_MEMORY_MB=4096 +VM_CPUS=4 +VM_DISK_SIZE=12G +SSH_PORT=2222 +SERIAL_PORT=4444 +CACHE_DIR= + +while getopts "j:z:i:v:o:u:m:c:p:s:k:r:" opt; do + case $opt in + j) JAR_FILE=$OPTARG ;; + z) ZIP_FILE=$OPTARG ;; + i) IMG_NAME=$OPTARG ;; + v) PG_VERSION=$OPTARG ;; + o) DOCKER_OPTS=$OPTARG ;; + u) VM_IMAGE_URL=$OPTARG ;; + m) VM_MEMORY_MB=$OPTARG ;; + c) VM_CPUS=$OPTARG ;; + p) SSH_PORT=$OPTARG ;; + s) VM_DISK_SIZE=$OPTARG ;; + k) CACHE_DIR=$OPTARG ;; + r) SERIAL_PORT=$OPTARG ;; + \?) exit 1 ;; + esac +done + +if [ -z "${JAR_FILE:-}" ] ; then + echo "Jar file parameter is required!" && exit 1; +fi +if [ -z "${ZIP_FILE:-}" ] ; then + echo "Zip file parameter is required!" && exit 1; +fi +if [ -z "${PG_VERSION:-}" ] ; then + echo "Postgres version parameter is required!" && exit 1; +fi +if [ -z "${IMG_NAME:-}" ] ; then + echo "Docker image parameter is required!" && exit 1; +fi +if [ -z "${VM_IMAGE_URL:-}" ] ; then + echo "FreeBSD VM image URL parameter is required!" && exit 1; +fi + +PROVISION_MODE= +if echo "$VM_IMAGE_URL" | grep -q 'BASIC-CLOUDINIT'; then + PROVISION_MODE=cloudinit +elif echo "$VM_IMAGE_URL" | grep -Eq 'disc1\.iso(\.xz)?$'; then + PROVISION_MODE=installer +else + echo "FreeBSD VM image URL must point to either a BASIC-CLOUDINIT image or a release disc1.iso installer image." && exit 1; +fi + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +LIB_DIR=$PWD +CACHE_DIR=${CACHE_DIR:-$PWD/.cache/freebsd-builder} +mkdir -p "$CACHE_DIR" + +if [ ! -f "$LIB_DIR/$JAR_FILE" ] ; then + if [ ! -f "$JAR_FILE" ] ; then + echo "Jar file not found: $JAR_FILE" && exit 1; + fi + JAR_HOST_PATH="$JAR_FILE" +else + JAR_HOST_PATH="$LIB_DIR/$JAR_FILE" +fi + +JAR_HOST_DIR=$(cd "$(dirname "$JAR_HOST_PATH")" && pwd) +JAR_HOST_BASENAME=$(basename "$JAR_HOST_PATH") +JAR_GUEST_NAME=$(basename "$JAR_FILE") + +docker run -i --rm \ + -v "${LIB_DIR}:/usr/local/pg-lib:ro" \ + -v "${JAR_HOST_DIR}:/usr/local/pg-jar-src:ro" \ + -v "${CACHE_DIR}:/usr/local/pg-cache" \ + -v "${SCRIPT_DIR}:/usr/local/pg-scripts:ro" \ + -e IMG_NAME="$IMG_NAME" \ + -e JAR_FILE="$JAR_FILE" \ + -e JAR_HOST_BASENAME="$JAR_HOST_BASENAME" \ + -e JAR_GUEST_NAME="$JAR_GUEST_NAME" \ + -e ZIP_FILE="$ZIP_FILE" \ + -e PG_VERSION="$PG_VERSION" \ + -e PROVISION_MODE="$PROVISION_MODE" \ + -e VM_IMAGE_URL="$VM_IMAGE_URL" \ + -e VM_MEMORY_MB="$VM_MEMORY_MB" \ + -e VM_CPUS="$VM_CPUS" \ + -e VM_DISK_SIZE="$VM_DISK_SIZE" \ + -e SSH_PORT="$SSH_PORT" \ + -e SERIAL_PORT="$SERIAL_PORT" \ + $DOCKER_OPTS "$IMG_NAME" /bin/bash -eu -c ' + log() { + echo "[freebsd-test] $*" + } + + wait_for_ssh() { + local user=$1 + local max_attempts=$2 + + log "Waiting for SSH from FreeBSD guest as ${user}" + for attempt in $(seq 1 "$max_attempts"); do + if ssh $SSH_OPTS "${user}@127.0.0.1" true >/dev/null 2>&1; then + log "SSH is reachable as ${user}" + return 0 + fi + if [ $((attempt % 6)) -eq 0 ]; then + log "SSH not ready yet after $((attempt * 5))s" + tail -n 20 "$WORK_DIR/serial.log" || true + fi + sleep 5 + done + + echo "FreeBSD guest did not become reachable over SSH as ${user}" >&2 + tail -n 200 "$WORK_DIR/serial.log" >&2 || true + exit 1 + } + + run_guest_test() { + local user=$1 + local runner=$2 + + log "Copying bundle and guest test helper" + scp $SCP_OPTS "/usr/local/pg-jar-src/${JAR_HOST_BASENAME}" "${user}@127.0.0.1:/tmp/${JAR_GUEST_NAME}" + scp $SCP_OPTS /usr/local/pg-scripts/test-postgres-freebsd-guest.sh "${user}@127.0.0.1:/tmp/test-postgres-freebsd-guest.sh" + + log "Running PostgreSQL smoke test inside FreeBSD guest" + if [ -n "$runner" ]; then + ssh $SSH_OPTS "${user}@127.0.0.1" "chmod +x /tmp/test-postgres-freebsd-guest.sh && ${runner} env JAR_FILE=/tmp/${JAR_GUEST_NAME} ZIP_FILE=${ZIP_FILE} PG_VERSION=${PG_VERSION} /tmp/test-postgres-freebsd-guest.sh" + else + ssh $SSH_OPTS "${user}@127.0.0.1" "chmod +x /tmp/test-postgres-freebsd-guest.sh && env JAR_FILE=/tmp/${JAR_GUEST_NAME} ZIP_FILE=${ZIP_FILE} PG_VERSION=${PG_VERSION} /tmp/test-postgres-freebsd-guest.sh" + fi + } + + export DEBIAN_FRONTEND=noninteractive + log "Installing host-side dependencies inside Docker image $IMG_NAME" + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + expect \ + netcat-openbsd \ + openssh-client \ + qemu-system-x86 \ + qemu-utils \ + xorriso \ + xz-utils + + WORK_DIR=$(mktemp -d /tmp/freebsd-test.XXXXXX) + cleanup() { + local errcode=$? + if [ -f "$WORK_DIR/qemu.pid" ]; then + kill "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/qemu.pid")" 2>/dev/null || true + fi + if [ -f "$WORK_DIR/serial-driver.pid" ]; then + kill "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + wait "$(cat "$WORK_DIR/serial-driver.pid")" 2>/dev/null || true + fi + rm -rf "$WORK_DIR" + return $errcode + } + trap cleanup EXIT + + ssh-keygen -q -t ed25519 -N "" -f "$WORK_DIR/id_ed25519" + SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -p $SSH_PORT" + SCP_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i $WORK_DIR/id_ed25519 -P $SSH_PORT" + + if [ -e /dev/kvm ]; then + QEMU_ACCEL=kvm + QEMU_CPU=max + else + QEMU_ACCEL=tcg + QEMU_CPU=qemu64 + fi + + if [ "$PROVISION_MODE" = cloudinit ]; then + mkdir -p "$WORK_DIR/seed" + VM_CACHE_BASENAME=$(basename "$VM_IMAGE_URL") + if [ ! -f "/usr/local/pg-cache/${VM_CACHE_BASENAME}" ]; then + log "Downloading FreeBSD cloud image from $VM_IMAGE_URL" + curl -fsSL "$VM_IMAGE_URL" -o "/usr/local/pg-cache/${VM_CACHE_BASENAME}" + else + log "Using cached FreeBSD cloud image ${VM_CACHE_BASENAME}" + fi + if echo "$VM_IMAGE_URL" | grep -q "\.xz$"; then + log "Decompressing FreeBSD cloud image" + xz -dc "/usr/local/pg-cache/${VM_CACHE_BASENAME}" > "$WORK_DIR/freebsd-base.qcow2" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-base.qcow2" + fi + + log "Creating writable overlay" + qemu-img create -q -f qcow2 -F qcow2 -b "$WORK_DIR/freebsd-base.qcow2" "$WORK_DIR/freebsd-overlay.qcow2" + + cat > "$WORK_DIR/seed/meta-data" < "$WORK_DIR/seed/user-data" < "$WORK_DIR/freebsd-installer.iso" + else + cp "/usr/local/pg-cache/${VM_CACHE_BASENAME}" "$WORK_DIR/freebsd-installer.iso" + fi + + cat > "$WORK_DIR/install-media/etc/installerconfig" < /root/.ssh/authorized_keys <<'"'"'KEYEOF'"'"' +$(cat "$WORK_DIR/id_ed25519.pub") +KEYEOF +chmod 600 /root/.ssh/authorized_keys +cat > /boot.config <<'"'"'BOOTEOF'"'"' +-Dh +BOOTEOF +cat > /boot/loader.conf <<'"'"'LOADERCONFEOF'"'"' +console="comconsole,vidconsole" +boot_multicons="YES" +autoboot_delay="1" +LOADERCONFEOF +printf "\nPermitRootLogin yes\nPasswordAuthentication no\nChallengeResponseAuthentication no\n" >> /etc/ssh/sshd_config +EOF + + cat > "$WORK_DIR/install-media/boot.config" < "$WORK_DIR/install-media/boot/loader.conf" < "$WORK_DIR/drive-serial.expect" < "$WORK_DIR/serial-driver.pid" + + wait_for_ssh root 360 + run_guest_test root "" + fi + + log "FreeBSD smoke test completed" + ' diff --git a/settings.gradle b/settings.gradle index 8bd3d01..369efb6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,5 @@ include 'alpine-lite-platforms' include 'custom-debian-platform' include 'custom-alpine-platform' -include 'custom-alpine-lite-platform' \ No newline at end of file +include 'custom-alpine-lite-platform' +include 'custom-freebsd-platform'