Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,13 @@ jobs:
- name: Build plugin binaries
env:
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
NOTARY_PROFILE: TablePro
NOTARIZE: "true"
TARGET: ${{ steps.plugin.outputs.target }}
VERSION: ${{ steps.plugin.outputs.version }}
run: |
./scripts/build-plugin.sh "$TARGET" arm64 "$VERSION"
./scripts/build-plugin.sh "$TARGET" x86_64 "$VERSION"
# One invocation, not one per architecture. The script has always taken "both", and
# each invocation regenerates both Xcode projects and wipes build/DerivedData, so calling
# it twice threw away everything the first architecture had compiled.
run: ./scripts/build-plugin.sh "$TARGET" both "$VERSION"

# Assert the result rather than trusting the flag. The previous notarization step was
# gated on an environment variable nothing defined, so it never ran and nothing noticed
Expand Down
8 changes: 1 addition & 7 deletions scripts/build-hiredis.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,14 @@ ARCH="${1:-both}"
# deleted when the driver moved into a plugin bundle, so this script was installing them where
# nothing read them and leaving the real ones untouched.
HEADER_DIR="$REPO_ROOT/Plugins/RedisDriverPlugin/CRedis/include/hiredis"
BUILD_DIR="$(mktemp -d)"
NCPU=$(sysctl -n hw.ncpu)
make_build_dir

echo "🔧 Building static hiredis $HIREDIS_VERSION + OpenSSL $OPENSSL_VERSION"
echo " Deployment target: macOS $DEPLOY_TARGET"
echo " Architecture: $ARCH"
echo " Build dir: $BUILD_DIR"
echo ""

cleanup() {
echo "🧹 Cleaning up build directory..."
rm -rf "$BUILD_DIR"
}
trap cleanup EXIT

download_sources() {
echo "📥 Downloading source tarballs..."
Expand Down
8 changes: 1 addition & 7 deletions scripts/build-libmongoc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,14 @@ MONGOC_SHA256="a93259840f461b28e198311e32144f5f8dc9fbd74348029f2793774d781bb7da"
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/macos.sh"

ARCH="${1:-both}"
BUILD_DIR="$(mktemp -d)"
NCPU=$(sysctl -n hw.ncpu)
make_build_dir

echo "🔧 Building static libmongoc $MONGOC_VERSION + OpenSSL $OPENSSL_VERSION"
echo " Deployment target: macOS $DEPLOY_TARGET"
echo " Architecture: $ARCH"
echo " Build dir: $BUILD_DIR"
echo ""

cleanup() {
echo "🧹 Cleaning up build directory..."
rm -rf "$BUILD_DIR"
}
trap cleanup EXIT

download_sources() {
echo "📥 Downloading source tarballs..."
Expand Down
8 changes: 1 addition & 7 deletions scripts/build-libpq.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,14 @@ ARCH="${1:-both}"
# TablePro/Core/Database/CLibPQ/include, which no longer exists, so every rebuild recreated a dead
# directory and never updated the headers the plugin actually compiles against.
HEADER_DIR="$REPO_ROOT/Plugins/PostgreSQLDriverPlugin/CLibPQ/include"
BUILD_DIR="$(mktemp -d)"
NCPU=$(sysctl -n hw.ncpu)
make_build_dir

echo "🔧 Building static libpq $PG_VERSION + OpenSSL $OPENSSL_VERSION"
echo " Deployment target: macOS $DEPLOY_TARGET"
echo " Architecture: $ARCH"
echo " Build dir: $BUILD_DIR"
echo ""

cleanup() {
echo "🧹 Cleaning up build directory..."
rm -rf "$BUILD_DIR"
}
trap cleanup EXIT

download_sources() {
echo "📥 Downloading source tarballs..."
Expand Down
8 changes: 1 addition & 7 deletions scripts/build-libssh2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,14 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/macos.sh"
LIBSSH2_SHA256="d9ec76cbe34db98eec3539fe2c899d26b0c837cb3eb466a56b0f109cabf658f7"

ARCH="${1:-both}"
BUILD_DIR="$(mktemp -d)"
NCPU=$(sysctl -n hw.ncpu)
make_build_dir

echo "🔧 Building static libssh2 $LIBSSH2_VERSION + OpenSSL $OPENSSL_VERSION"
echo " Deployment target: macOS $DEPLOY_TARGET"
echo " Architecture: $ARCH"
echo " Build dir: $BUILD_DIR"
echo ""

cleanup() {
echo "🧹 Cleaning up build directory..."
rm -rf "$BUILD_DIR"
}
trap cleanup EXIT

download_sources() {
echo "📥 Downloading source tarballs..."
Expand Down
5 changes: 1 addition & 4 deletions scripts/build-mariadb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,13 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/macos.sh"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
LIBS_DIR="$PROJECT_DIR/Libs"
BUILD_DIR="$(mktemp -d)"
NCPU=$(sysctl -n hw.ncpu)
make_build_dir

if [ -z "$OPENSSL_ROOT" ] || [ ! -d "$OPENSSL_ROOT" ]; then
echo "ERROR: OpenSSL 3 not found. Install with 'brew install openssl@3' or set OPENSSL_ROOT." >&2
exit 1
fi

cleanup() { rm -rf "$BUILD_DIR"; }
trap cleanup EXIT

echo "Building MariaDB Connector/C $MARIADB_VERSION for macOS (OpenSSL: $OPENSSL_ROOT)"

Expand Down
66 changes: 10 additions & 56 deletions scripts/build-plugin.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/notarize.sh"

# Build script for creating standalone plugin bundles
# Usage: ./scripts/build-plugin.sh <PluginTarget> [arm64|x86_64|both] [version]
# Example: ./scripts/build-plugin.sh OracleDriverPlugin arm64 1.0.0
Expand All @@ -19,10 +21,6 @@ BUILD_DIR="build/Plugins"
SIGN_IDENTITY="${SIGN_IDENTITY:-}"
TEAM_ID="${TEAM_ID:-}"
NOTARIZE="${NOTARIZE:-false}"
APPLE_ID="${APPLE_ID:-}"
# The workflow's "Configure notarization" step stores its credentials under this
# name. A local build that keeps its own profile can override it.
NOTARY_PROFILE="${NOTARY_PROFILE:-TablePro}"

if [ -z "$TEAM_ID" ]; then
echo "ERROR: TEAM_ID is not set. Pass via env or set in your shell profile." >&2
Expand Down Expand Up @@ -178,59 +176,15 @@ create_zip() {
# published before this ran was unnotarized, because the workflow gated the step on an
# environment variable nothing ever set.
#
# notarytool only accepts an archive, so the bundle is zipped to a throwaway path for the
# submission. The ticket then has to be stapled into the bundle itself, or every user needs
# a live round trip to Apple on first load and an offline Mac never gets one. Stapling
# rewrites the bundle, so the distribution zip and its SHA-256 must both be produced after
# it: the registry manifest pins that checksum and PluginInstaller rejects a mismatch.
notarize_and_staple() {
local plugin_path=$1

# Stapling rewrites the bundle, so the distribution zip and its SHA-256 must both be produced
# after it: the registry manifest pins that checksum and PluginInstaller rejects a mismatch.
notarize_plugin() {
if [ "$NOTARIZE" != "true" ]; then
echo "Skipping notarization (set NOTARIZE=true to enable)"
return
fi

if [ -z "$APPLE_ID" ]; then
echo "ERROR: APPLE_ID is not set but NOTARIZE=true." >&2
echo " Pass APPLE_ID=<your-apple-id>, and store credentials with" >&2
echo " xcrun notarytool store-credentials \"$NOTARY_PROFILE\"." >&2
exit 1
fi

local submission_zip
submission_zip="$(mktemp -d)/$(basename "$plugin_path" .tableplugin)-notarize.zip"
ditto -c -k --keepParent "$plugin_path" "$submission_zip"

echo "Submitting $(basename "$plugin_path") for notarization..."
if ! xcrun notarytool submit "$submission_zip" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--keychain-profile "$NOTARY_PROFILE" \
--wait; then
echo "FATAL: Notarization failed for $plugin_path" >&2
exit 1
fi

echo "Stapling the ticket into the bundle..."
if ! xcrun stapler staple "$plugin_path"; then
echo "FATAL: Stapling failed for $plugin_path" >&2
exit 1
fi

if ! xcrun stapler validate "$plugin_path"; then
echo "FATAL: The stapled ticket did not validate for $plugin_path" >&2
exit 1
fi

# spctl is what a user's Mac runs. A pass here is the only proof the bundle will load.
if ! spctl -a -vvv -t open --context context:primary-signature "$plugin_path" 2>&1 | grep -q "accepted"; then
echo "FATAL: Gatekeeper still rejects $plugin_path after notarization" >&2
spctl -a -vvv -t open --context context:primary-signature "$plugin_path" || true
exit 1
fi

echo "Notarized, stapled and accepted by Gatekeeper"
# "open", not "exec": a plugin bundle is opened by the app, not launched.
notarize_and_staple "$1" open
}

# TablePro.xcodeproj is generated and not in git, so a fresh checkout has none.
Expand All @@ -244,15 +198,15 @@ mkdir -p "$BUILD_DIR"
case "$ARCH" in
arm64|x86_64)
plugin_path=$(build_plugin "$ARCH")
notarize_and_staple "$plugin_path"
notarize_plugin "$plugin_path"
create_zip "$plugin_path" "$ARCH"
;;
both)
arm64_path=$(build_plugin "arm64")
x86_path=$(build_plugin "x86_64")

notarize_and_staple "$arm64_path"
notarize_and_staple "$x86_path"
notarize_plugin "$arm64_path"
notarize_plugin "$x86_path"

create_zip "$arm64_path" "arm64"
create_zip "$x86_path" "x86_64"
Expand Down
32 changes: 2 additions & 30 deletions scripts/build-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ set -euo pipefail

# shellcheck source=lib/macos.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/macos.sh"
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/notarize.sh"

# Build script for creating architecture-specific releases
# Usage: ./build-release.sh [arm64|x86_64|both]
Expand Down Expand Up @@ -492,38 +493,9 @@ fi
if [ "$NOTARIZE" = "true" ]; then
echo ""
echo "📮 Notarizing..."

# Requires: xcrun notarytool store-credentials "TablePro" --apple-id ... --team-id ... --password ...
for app in "$BUILD_DIR"/TablePro-*.app; do
[ -d "$app" ] || continue
name=$(basename "$app")
zip_path="$BUILD_DIR/${name%.app}.zip"
echo " Zipping $name..."
ditto -c -k --keepParent "$app" "$zip_path"

echo " Submitting $name for notarization..."
# Assigned before the substitution, then overwritten on failure. Written the other way
# round, `submit_status=$?` reads the exit status of the assignment, which is always 0,
# so every failure took the success branch and the log fetch below was unreachable.
submit_status=0
submit_output=$(xcrun notarytool submit "$zip_path" --keychain-profile "TablePro" --wait 2>&1) || submit_status=$?
echo "$submit_output"

submission_id=$(echo "$submit_output" | grep "id:" | head -1 | awk '{print $2}')

if [ $submit_status -eq 0 ] && echo "$submit_output" | grep -q "status: Accepted"; then
echo " Stapling $name..."
xcrun stapler staple "$app"
echo " ✅ $name notarized and stapled"
else
echo " ❌ Notarization failed for $name"
if [ -n "$submission_id" ]; then
echo " 📋 Fetching notarization log for $submission_id..."
xcrun notarytool log "$submission_id" --keychain-profile "TablePro" 2>&1 || true
fi
exit 1
fi
rm -f "$zip_path"
notarize_and_staple "$app" exec
done
echo "✅ Notarization complete"
fi
13 changes: 2 additions & 11 deletions scripts/create-dmg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,8 @@ echo "✅ DMG signed"

# Notarize the DMG (opt-in via NOTARIZE=true)
if [ "$NOTARIZE" = "true" ]; then
echo "📮 Notarizing DMG..."
if xcrun notarytool submit "$FINAL_DMG" --keychain-profile "TablePro" --wait; then
xcrun stapler staple "$FINAL_DMG"
# Stapling can report success and still leave no usable ticket, which is why
# build-plugin.sh validates after stapling. The DMG had no such check.
xcrun stapler validate "$FINAL_DMG"
echo "✅ DMG notarized and stapled"
else
echo "❌ DMG notarization failed"
exit 1
fi
# "open", not "exec": a user opens a disk image, they do not launch it.
notarize_and_staple "$FINAL_DMG" open
fi

# Get final size
Expand Down
14 changes: 14 additions & 0 deletions scripts/lib/macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,17 @@ prepare_arch_libs() {
return 1
fi
}

# Every library build wants one private scratch directory that goes away on exit. Five scripts
# each declared their own BUILD_DIR, an identical three-line cleanup() and the same trap; one of
# them had leaked its directory on every run until #2352.
#
# The trap body is single-quoted so it reads BUILD_DIR when it fires, not when it is installed.
# It replaces any EXIT trap already in place, so a script that needs to remove more than this
# directory installs its own instead (build-freetds.sh does, for its tarball cache).
make_build_dir() {
BUILD_DIR="$(mktemp -d)"
trap 'rm -rf "$BUILD_DIR"' EXIT
}

NCPU="$(sysctl -n hw.ncpu)"
82 changes: 82 additions & 0 deletions scripts/lib/notarize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Submit an artifact to Apple's notary service, staple the ticket, and prove it will open.
# Source this; it defines notarize_and_staple and sets nothing else.
#
# This existed three times at three rigor levels. build-plugin.sh stapled, validated and asked
# Gatekeeper; create-dmg.sh stapled and validated; build-release.sh only stapled, and it was the
# only one that fetched the notary log when a submission failed. So the app, the artifact users
# actually download, got the weakest checks and the DMG and plugins got the best diagnostics
# withheld. One implementation, at the strictest level all three reached between them.
#
# The two post-staple checks are not redundant. spctl asks Gatekeeper, which will fetch the
# ticket from Apple over the network, so an artifact that was notarized but never successfully
# stapled still passes: /Applications/Ghostty.app has no ticket and spctl accepts it. Only
# `stapler validate` proves the ticket is in the artifact, which is what an offline Mac needs.

# The apple-signing action's "Configure notarization" step stores the Apple ID, team and
# app-specific password under this profile name, so --keychain-profile is the whole credential
# and passing --apple-id or --team-id alongside it is redundant. A local build with its own
# stored profile can override the name.
NOTARY_PROFILE="${NOTARY_PROFILE:-TablePro}"

# Usage: notarize_and_staple <path> [exec|open]
#
# The second argument is the Gatekeeper assessment type: "exec" for an application, "open" for a
# disk image or a plugin bundle, which a user opens rather than launches.
notarize_and_staple() {
local path="${1:?notarize_and_staple needs a path}"
local assessment="${2:-exec}"
local name
name="$(basename "$path")"

[ -e "$path" ] || { echo "FATAL: nothing to notarize at $path" >&2; return 1; }

# notarytool only accepts an archive, so a bundle is zipped to a throwaway path. The ticket
# is then stapled into the bundle itself, not the zip: without that every user needs a live
# round trip to Apple on first load and an offline Mac never gets one. Stapling rewrites the
# artifact, so any distribution zip and its checksum must be produced after this returns.
local submission="$path" scratch=""
if [ -d "$path" ]; then
scratch="$(mktemp -d)"
submission="$scratch/${name}.zip"
ditto -c -k --keepParent "$path" "$submission"
fi

echo "Submitting $name for notarization..."
# Assigned before the substitution, then overwritten on failure. Written the other way round,
# `status=$?` reads the exit status of the assignment, which is always 0, so every failure
# took the success branch and the log fetch below was unreachable.
local submit_status=0 output=""
output="$(xcrun notarytool submit "$submission" --keychain-profile "$NOTARY_PROFILE" --wait 2>&1)" || submit_status=$?
echo "$output"
[ -z "$scratch" ] || rm -rf "$scratch"

# Both checks, because notarytool has shipped versions that exit 0 on a rejected submission.
if [ "$submit_status" -ne 0 ] || ! echo "$output" | grep -q "status: Accepted"; then
echo "FATAL: notarization failed for $name" >&2
local submission_id
submission_id="$(echo "$output" | grep "id:" | head -1 | awk '{print $2}')"
if [ -n "$submission_id" ]; then
echo "Notary log for $submission_id:" >&2
xcrun notarytool log "$submission_id" --keychain-profile "$NOTARY_PROFILE" >&2 2>&1 || true
fi
return 1
fi

echo "Stapling the ticket into $name..."
xcrun stapler staple "$path" || { echo "FATAL: stapling failed for $name" >&2; return 1; }

# Stapling can report success and still leave no usable ticket.
xcrun stapler validate "$path" || { echo "FATAL: the stapled ticket did not validate for $name" >&2; return 1; }

# spctl is what a user's Mac runs. A pass here is the only proof the artifact will open.
local assessment_output
if ! assessment_output="$(spctl -a -vvv -t "$assessment" --context context:primary-signature "$path" 2>&1)" \
|| ! echo "$assessment_output" | grep -q "accepted"; then
echo "FATAL: Gatekeeper still rejects $name after notarization" >&2
echo "$assessment_output" >&2
return 1
fi

echo "$name notarized, stapled and accepted by Gatekeeper"
}
Loading