Skip to content

update to new logo icon #1408

update to new logo icon

update to new logo icon #1408

Workflow file for this run

name: CI
on:
push:
pull_request:
jobs:
macos-26:
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app
- name: Install dependencies
run: |
brew install automake autoconf pkg-config libtool \
berkeley-db@4 zeromq miniupnpc boost \
gperf qrencode librsvg \
openssl@4 libevent protobuf
- name: Link brew deps
run: brew link berkeley-db@4 boost
- name: Install Qt 6.10.2 for macOS
run: |
set -euo pipefail
python3 -m venv "$RUNNER_TEMP/aqt-venv"
source "$RUNNER_TEMP/aqt-venv/bin/activate"
python -m pip install --upgrade pip aqtinstall
QT_ROOT="$RUNNER_TEMP/qt"
python -m aqt install-qt mac desktop 6.10.2 clang_64 \
--outputdir "$QT_ROOT" \
-m qtwebengine qtwebchannel qtpositioning qt5compat
QMAKE_PATH="$(find "$QT_ROOT" -type f \( -name qmake6 -o -name qmake \) -path "*/bin/*" | head -n1 || true)"
if [ -z "${QMAKE_PATH:-}" ]; then
echo "ERROR: Failed to resolve qmake under $QT_ROOT"
find "$QT_ROOT" -maxdepth 5 -type d | sed -n '1,200p'
exit 1
fi
QT_PREFIX="$(dirname "$(dirname "$QMAKE_PATH")")"
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
echo "Installed Qt prefix: $QT_PREFIX"
- name: Add Qt tools to PATH
run: |
echo "$QT_PREFIX/bin" >> "$GITHUB_PATH"
echo "$QT_PREFIX/libexec" >> "$GITHUB_PATH"
- name: Resolve Qt host tools
run: |
set -euo pipefail
ALIAS_DIR="$PWD/.qt-tool-aliases"
mkdir -p "$ALIAS_DIR"
declare -a QT_TOOL_DIRS=(
"$QT_PREFIX/libexec"
"$QT_PREFIX/bin"
)
QMAKE_BIN="$(command -v qmake6 || command -v qmake || true)"
if [ -n "$QMAKE_BIN" ]; then
for qdir in \
"$("$QMAKE_BIN" -query QT_HOST_BINS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_INSTALL_BINS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_HOST_LIBEXECS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_INSTALL_LIBEXECS 2>/dev/null || true)"
do
if [ -n "$qdir" ]; then
QT_TOOL_DIRS+=("$qdir")
fi
done
fi
find_qt_tool() {
local base="$1"
shift || true
local names=("$base" "${base}6" "${base}-qt6")
local d n cand
for d in "${QT_TOOL_DIRS[@]}"; do
[ -d "$d" ] || continue
for n in "${names[@]}"; do
cand="$d/$n"
if [ -x "$cand" ]; then
echo "$cand"
return 0
fi
done
done
return 1
}
MOC_REAL="$(find_qt_tool moc || true)"
UIC_REAL="$(find_qt_tool uic || true)"
RCC_REAL="$(find_qt_tool rcc || true)"
if [ -z "$MOC_REAL" ] || [ -z "$UIC_REAL" ] || [ -z "$RCC_REAL" ]; then
echo "ERROR: Could not resolve Qt host tools."
echo "QMAKE_BIN=${QMAKE_BIN:-<none>}"
echo "QT tool dirs searched:"
printf ' - %s\n' "${QT_TOOL_DIRS[@]}"
echo "Listing candidate dirs:"
for d in "${QT_TOOL_DIRS[@]}"; do
if [ -d "$d" ]; then
echo "-- $d"
ls -la "$d" | egrep 'moc|uic|rcc|qmake' || true
fi
done
exit 1
fi
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc"
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc-qt6"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic-qt6"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc-qt6"
echo "$ALIAS_DIR" >> "$GITHUB_PATH"
echo "MOC=$ALIAS_DIR/moc" >> "$GITHUB_ENV"
echo "UIC=$ALIAS_DIR/uic" >> "$GITHUB_ENV"
echo "RCC=$ALIAS_DIR/rcc" >> "$GITHUB_ENV"
- name: Verify Qt tools
run: |
set -euo pipefail
which moc || true
which moc6 || true
ls -l "$PWD/.qt-tool-aliases" || true
ls -l "$QT_PREFIX/bin" | egrep 'moc|moc6|uic|rcc|qmake' || true
ls -l "$QT_PREFIX/libexec" | egrep 'moc|moc6|uic|rcc|qmake' || true
moc -v
- name: Autogen
run: ./autogen.sh
- name: Configure
run: |
set -euo pipefail
BREW_PREFIX="$(brew --prefix)"
BOOST_PREFIX="$(brew --prefix boost)"
BDB_PREFIX="$(brew --prefix berkeley-db@4)"
OPENSSL_PREFIX="$(brew --prefix openssl@4)"
LIBEVENT_PREFIX="$(brew --prefix libevent)"
PROTOBUF_PREFIX="$(brew --prefix protobuf)"
QMAKE_BIN="$(command -v qmake6 || command -v qmake)"
QT_INCLUDE_DIR="$("$QMAKE_BIN" -query QT_INSTALL_HEADERS)"
QT_LIB_DIR="$("$QMAKE_BIN" -query QT_INSTALL_LIBS)"
QT_PLUGIN_DIR="$("$QMAKE_BIN" -query QT_INSTALL_PLUGINS)"
export CPPFLAGS="-I$PROTOBUF_PREFIX/include -I$BOOST_PREFIX/include -I$BDB_PREFIX/include -I$BREW_PREFIX/include ${CPPFLAGS:-}"
export LDFLAGS="-L$PROTOBUF_PREFIX/lib -L$BOOST_PREFIX/lib -L$BDB_PREFIX/lib -L$BREW_PREFIX/lib ${LDFLAGS:-}"
export PKG_CONFIG_PATH="$PROTOBUF_PREFIX/lib/pkgconfig:$QT_LIB_DIR/pkgconfig:$BOOST_PREFIX/lib/pkgconfig:$BDB_PREFIX/lib/pkgconfig:$BREW_PREFIX/lib/pkgconfig:$BREW_PREFIX/share/pkgconfig:${PKG_CONFIG_PATH:-}"
export CXXFLAGS="-std=c++17 -D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION -Wno-deprecated-builtins -Wno-deprecated-declarations"
export OBJCXXFLAGS="$CXXFLAGS"
export CFLAGS="-Wno-deprecated-builtins -Wno-deprecated-declarations"
./configure \
--disable-tests \
--disable-bench \
--disable-werror \
--with-gui=qt6 \
--with-qt-bindir="$PWD/.qt-tool-aliases" \
--with-qt-incdir="$QT_INCLUDE_DIR" \
--with-qt-libdir="$QT_LIB_DIR" \
--with-qt-plugindir="$QT_PLUGIN_DIR" \
--with-boost="$BOOST_PREFIX" \
--bindir="$(pwd)/release/bin" \
--libdir="$(pwd)/release/lib" \
--with-openssl-dir="$OPENSSL_PREFIX" \
--with-libevent-dir="$LIBEVENT_PREFIX"
- name: Build
run: make -j4
- name: Test verged daemon
run: |
VERGED_BIN="./src/verged"
CLI_BIN="./src/verge-cli"
if [ ! -f "$VERGED_BIN" ]; then
echo "::error::verged not found at $VERGED_BIN"
exit 1
fi
# Run daemon in background
"$VERGED_BIN" -daemon
# Give it a few seconds to start
sleep 5
# Verify it's running
if ! pgrep -f verged >/dev/null; then
echo "::error::verged failed to start"
exit 1
fi
# Query info with CLI
"$CLI_BIN" getinfo || true
# Stop the daemon
"$CLI_BIN" stop
- name: Test verge-qt directly
run: |
QT_BIN="./src/qt/verge-qt"
export DYLD_FRAMEWORK_PATH="$QT_PREFIX/lib:${DYLD_FRAMEWORK_PATH:-}"
export DYLD_LIBRARY_PATH="$QT_PREFIX/lib:${DYLD_LIBRARY_PATH:-}"
if [ ! -f "$QT_BIN" ]; then
echo "::error::verge-qt not found at $QT_BIN"
exit 1
fi
chmod +x "$QT_BIN"
echo "Launching verge-qt..."
"$QT_BIN" &
sleep 5
if ! pgrep -f verge-qt >/dev/null; then
echo "::error::verge-qt failed to start"
exit 1
fi
echo "verge-qt started successfully"
pkill verge-qt || true
- name: Upload verge-qt raw binary
uses: actions/upload-artifact@v4
with:
name: verge-qt-raw
path: src/qt/verge-qt
if-no-files-found: error
- name: Wrap binary into .app bundle
run: |
resolve_macho_binary() {
local candidate="$1"
if [ -f "$candidate" ] && file "$candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$candidate"
return 0
fi
local libtool_candidate
libtool_candidate="$(find "$(dirname "$candidate")" -path "*/.libs/$(basename "$candidate")" -type f | head -n1)"
if [ -n "$libtool_candidate" ] && file "$libtool_candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$libtool_candidate"
return 0
fi
return 1
}
BINARY="./src/qt/verge-qt"
if [ ! -f "$BINARY" ]; then
BINARY="$(find . -path '*/src/qt/verge-qt' -type f | head -n1)"
fi
if [ -z "$BINARY" ] || [ ! -f "$BINARY" ]; then
echo "::error::verge-qt binary not found after make"
exit 1
fi
MACHO_BINARY="$(resolve_macho_binary "$BINARY" || true)"
if [ -z "$MACHO_BINARY" ]; then
echo "::error::Unable to resolve a Mach-O verge-qt binary from $BINARY"
ls -la "$(dirname "$BINARY")" || true
file "$BINARY" || true
find "$(dirname "$BINARY")" -path '*/.libs/verge-qt' -type f -exec file {} \; || true
exit 1
fi
echo "Found verge-qt launcher at $BINARY"
echo "Using Mach-O binary at $MACHO_BINARY"
# Create .app bundle
APP_NAME="Verge-Qt.app"
mkdir -p "$APP_NAME/Contents/MacOS"
cp -fL "$MACHO_BINARY" "$APP_NAME/Contents/MacOS/verge-qt"
chmod +x "$APP_NAME/Contents/MacOS/verge-qt"
# Minimal Info.plist
echo '<?xml version="1.0" encoding="UTF-8"?>' > "$APP_NAME/Contents/Info.plist"
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' >> "$APP_NAME/Contents/Info.plist"
echo '<plist version="1.0">' >> "$APP_NAME/Contents/Info.plist"
echo '<dict>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleName</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>Verge</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleIdentifier</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>org.vergefoundation.verge-qt</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleVersion</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>1.0</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleExecutable</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>verge-qt</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundlePackageType</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>APPL</string>' >> "$APP_NAME/Contents/Info.plist"
echo '</dict>' >> "$APP_NAME/Contents/Info.plist"
echo '</plist>' >> "$APP_NAME/Contents/Info.plist"
- name: Bundle with macdeployqt
run: |
APP_NAME="Verge-Qt.app"
macdeployqt "$APP_NAME" -verbose=2
- name: Bundle QtWebEngine helper frameworks
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
HELPER_APP="$FRAMEWORKS_DIR/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app"
HELPER_BIN="$HELPER_APP/Contents/MacOS/QtWebEngineProcess"
if [ ! -x "$HELPER_BIN" ]; then
echo "::error::QtWebEngineProcess helper missing at $HELPER_BIN"
exit 1
fi
extract_framework_root() {
python3 -c 'import sys; path=sys.argv[1]; marker=".framework/"; idx=path.find(marker); print(path[:idx+len(".framework")] if idx >= 0 else "")' "$1"
}
framework_binary_name() {
python3 -c 'import os,sys; name=os.path.basename(sys.argv[1]); print(name[:-10] if name.endswith(".framework") else name)' "$1"
}
is_macho_binary() {
file "$1" 2>/dev/null | grep -Eq 'Mach-O'
}
is_homebrew_qt_framework_ref() {
case "$1" in
/opt/homebrew/*/Qt*.framework/*|/usr/local/*/Qt*.framework/*) return 0 ;;
*) return 1 ;;
esac
}
copy_qt_framework() {
local source_framework="$1"
local framework_name
framework_name="$(basename "$source_framework")"
local dest_framework="$FRAMEWORKS_DIR/$framework_name"
if [ -e "$dest_framework" ]; then
return 0
fi
if [ ! -d "$source_framework" ]; then
echo "::error::Missing Qt framework source: $source_framework"
exit 1
fi
cp -R "$source_framework" "$dest_framework"
chmod -R u+w "$dest_framework"
}
map_qt_refs() {
local binary="$1"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
if ! is_homebrew_qt_framework_ref "$dep_path"; then
continue
fi
local framework_root
framework_root="$(extract_framework_root "$dep_path")"
if [ -z "$framework_root" ]; then
echo "::error::Could not determine framework root for $dep_path"
exit 1
fi
local framework_name
framework_name="$(basename "$framework_root")"
copy_qt_framework "$framework_root"
local framework_binary
framework_binary="$(framework_binary_name "$framework_name")"
local new_ref
new_ref="$(python3 -c 'import os,sys; binary=sys.argv[1]; target=sys.argv[2]; print("@loader_path/" + os.path.relpath(target, os.path.dirname(binary)).replace(os.sep, "/"))' "$binary" "$FRAMEWORKS_DIR/$framework_name/Versions/A/$framework_binary")"
install_name_tool -change "$dep_path" "$new_ref" "$binary"
done < <(otool -L "$binary" | awk 'NR > 1 { print $1 }')
}
map_bundle_framework_refs() {
local binary="$1"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
case "$dep_path" in
@executable_path/../Frameworks/*)
local target_path=""
case "$dep_path" in
*@executable_path/../Frameworks/*.framework/*|@executable_path/../Frameworks/*.framework/*)
local framework_root
framework_root="$(extract_framework_root "$dep_path")"
[ -n "$framework_root" ] || continue
local framework_name
framework_name="$(basename "$framework_root")"
local framework_binary
framework_binary="$(framework_binary_name "$framework_name")"
target_path="$FRAMEWORKS_DIR/$framework_name/Versions/A/$framework_binary"
;;
*)
local dep_name
dep_name="$(basename "$dep_path")"
target_path="$FRAMEWORKS_DIR/$dep_name"
;;
esac
[ -e "$target_path" ] || continue
local new_ref
new_ref="$(python3 -c 'import os,sys; binary=sys.argv[1]; target=sys.argv[2]; print("@loader_path/" + os.path.relpath(target, os.path.dirname(binary)).replace(os.sep, "/"))' "$binary" "$target_path")"
install_name_tool -change "$dep_path" "$new_ref" "$binary"
;;
esac
done < <(otool -L "$binary" | awk 'NR > 1 { print $1 }')
}
map_qt_refs "$HELPER_BIN"
map_bundle_framework_refs "$HELPER_BIN"
while read -r bundled_bin; do
[ -n "$bundled_bin" ] || continue
is_macho_binary "$bundled_bin" || continue
map_qt_refs "$bundled_bin"
map_bundle_framework_refs "$bundled_bin"
done < <(find "$FRAMEWORKS_DIR" -type f)
add_rpath_if_missing() {
local binary="$1"
local rpath="$2"
if ! otool -l "$binary" | awk '
$1 == "cmd" && $2 == "LC_RPATH" { in_rpath=1; next }
in_rpath && $1 == "path" { print $2; in_rpath=0 }
' | grep -Fxq "$rpath"; then
install_name_tool -add_rpath "$rpath" "$binary"
fi
}
add_rpath_if_missing "$APP_BIN" "@executable_path/../Frameworks"
add_rpath_if_missing "$HELPER_BIN" "@executable_path/../../../../.."
if otool -L "$HELPER_BIN" | awk 'NR > 1 { print $1 }' | grep -Eq '^(/opt/homebrew|/usr/local)/.*Qt.*\.framework/'; then
echo "::error::QtWebEngineProcess still has Homebrew Qt framework references after bundling"
otool -L "$HELPER_BIN"
exit 1
fi
python3 - <<'PY'
import os
import re
import subprocess
import sys
frameworks_dir = os.path.join("Verge-Qt.app", "Contents", "Frameworks")
bad = []
pattern = re.compile(r'^@executable_path/\.\./Frameworks/')
for root, _, files in os.walk(frameworks_dir):
for name in files:
if "/Versions/A/" not in os.path.join(root, name).replace("\\", "/"):
continue
binary = os.path.join(root, name)
try:
file_output = subprocess.check_output(["file", binary], text=True)
except subprocess.CalledProcessError:
continue
if "Mach-O" not in file_output:
continue
try:
output = subprocess.check_output(["otool", "-L", binary], text=True)
except subprocess.CalledProcessError:
continue
lines = [line.strip() for line in output.splitlines()[1:]]
framework_name = None
if ".framework/Versions/A/" in binary:
framework_name = binary.split(".framework/Versions/A/")[0].split("/")[-1] + ".framework"
for line in lines:
dep = line.split(" ", 1)[0]
if not pattern.match(dep):
continue
if framework_name and dep == f"@executable_path/../Frameworks/{framework_name}/Versions/A/{name}":
continue
bad.append((binary, dep))
if bad:
print("::error::Bundled binaries still reference bundle deps via @executable_path")
for binary, dep in bad:
print(f"-- {binary}")
print(dep)
sys.exit(1)
PY
echo "QtWebEngine helper linkage:"
otool -L "$HELPER_BIN" | grep -E 'Qt(WebEngine|Qml|Quick|Core|Gui|Network|OpenGL)' || true
- name: Bundle transitive Boost dylibs
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
BOOST_LIB_DIR="$(brew --prefix boost)/lib"
mkdir -p "$FRAMEWORKS_DIR"
shopt -s nullglob
rm -f "$FRAMEWORKS_DIR"/libboost_*.dylib
resolve_boost_source() {
local dep_name="$1"
local candidate
for candidate in \
"$BOOST_LIB_DIR/$dep_name" \
"$BOOST_LIB_DIR/$dep_name".*.dylib \
"$BOOST_LIB_DIR/$dep_name"-*.dylib
do
[ -e "$candidate" ] || continue
candidate="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$candidate")"
[ -f "$candidate" ] || continue
if file "$candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$candidate"
return 0
fi
done
{
echo "::error::Unable to locate Boost dependency $dep_name in $BOOST_LIB_DIR"
ls -la "$BOOST_LIB_DIR" | grep libboost || true
} >&2
exit 1
}
copy_boost_dep() {
local dep_name="$1"
local source target
source="$(resolve_boost_source "$dep_name")"
target="$FRAMEWORKS_DIR/$dep_name"
cp -fL "$source" "$target"
chmod u+w "$target"
if ! file "$target" | grep -q 'Mach-O'; then
echo "::error::Copied Boost library is not Mach-O: $target"
ls -l "$target" || true
file "$target" || true
exit 1
fi
install_name_tool -id "@loader_path/$dep_name" "$target"
}
collect_boost_names() {
local file="$1"
otool -L "$file" | awk 'NR > 1 && /libboost_[^[:space:]]+\.dylib/ { n=$1; sub(/^.*\//, "", n); sub(/:$/, "", n); print n }'
}
declare -a queue=()
SEEN_FILE="$RUNNER_TEMP/boost-seen-$$.txt"
: > "$SEEN_FILE"
while read -r dep_name; do
[ -n "$dep_name" ] || continue
if ! grep -Fxq "$dep_name" "$SEEN_FILE"; then
printf '%s\n' "$dep_name" >> "$SEEN_FILE"
queue+=("$dep_name")
fi
done < <(collect_boost_names "$APP_BIN")
if [ "${#queue[@]}" -eq 0 ]; then
echo "::error::No Boost dylibs referenced by $APP_BIN"
otool -L "$APP_BIN" || true
exit 1
fi
index=0
while [ "$index" -lt "${#queue[@]}" ]; do
dep_name="${queue[$index]}"
copy_boost_dep "$dep_name"
while read -r child_name; do
[ -n "$child_name" ] || continue
if ! grep -Fxq "$child_name" "$SEEN_FILE"; then
printf '%s\n' "$child_name" >> "$SEEN_FILE"
queue+=("$child_name")
fi
done < <(collect_boost_names "$FRAMEWORKS_DIR/$dep_name")
index=$((index + 1))
done
rm -f "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@executable_path/../Frameworks/$dep_name" "$APP_BIN" || true
done < <(otool -L "$APP_BIN" | awk '/libboost_[^[:space:]]+\.dylib/ { print $1 }')
for lib in "$FRAMEWORKS_DIR"/libboost_*.dylib; do
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@loader_path/$dep_name" "$lib" || true
done < <(otool -L "$lib" | awk '/libboost_[^[:space:]]+\.dylib/ { print $1 }')
done
for file in "$APP_BIN" "$FRAMEWORKS_DIR"/libboost_*.dylib; do
echo "Boost linkage for $file"
otool -L "$file" | grep -E 'libboost_|verge-qt' || true
done
- name: Bundle locally built dylibs
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
WORKSPACE_ROOT="$(pwd)"
mkdir -p "$FRAMEWORKS_DIR"
collect_local_dep_paths() {
local file="$1"
otool -L "$file" | awk 'NR > 1 { print $1 }' | while read -r dep_path; do
[ -n "$dep_path" ] || continue
case "$dep_path" in
"$WORKSPACE_ROOT"/*.dylib)
printf '%s\n' "$dep_path"
;;
esac
done
}
resolve_local_dep_source() {
local source="$1"
local resolved dep_name candidate
if [ -e "$source" ]; then
resolved="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$source")"
if [ -f "$resolved" ] && file "$resolved" | grep -q 'Mach-O'; then
printf '%s\n' "$resolved"
return 0
fi
fi
dep_name="$(basename "$source")"
while read -r candidate; do
[ -n "$candidate" ] || continue
resolved="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$candidate")"
if [ -f "$resolved" ] && file "$resolved" | grep -q 'Mach-O'; then
printf '%s\n' "$resolved"
return 0
fi
done < <(find "$WORKSPACE_ROOT" \( -path "*/.libs/$dep_name" -o -path "*/$dep_name" \) \( -type f -o -type l \) 2>/dev/null)
return 1
}
copy_local_dep() {
local source="$1"
local dep_name target
source="$(resolve_local_dep_source "$source" || true)"
if [ -z "$source" ] || [ ! -f "$source" ]; then
echo "::error::Missing local dylib dependency at $source"
exit 1
fi
dep_name="$(basename "$source")"
target="$FRAMEWORKS_DIR/$dep_name"
cp -fL "$source" "$target"
chmod u+w "$target"
if ! file "$target" | grep -q 'Mach-O'; then
echo "::error::Copied local dependency is not Mach-O: $target"
ls -l "$target" || true
file "$target" || true
exit 1
fi
install_name_tool -id "@loader_path/$dep_name" "$target"
}
declare -a queue=()
SEEN_FILE="$RUNNER_TEMP/local-dylib-seen-$$.txt"
: > "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_path="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$dep_path")"
if ! grep -Fxq "$dep_path" "$SEEN_FILE"; then
printf '%s\n' "$dep_path" >> "$SEEN_FILE"
queue+=("$dep_path")
fi
done < <(collect_local_dep_paths "$APP_BIN")
index=0
while [ "$index" -lt "${#queue[@]}" ]; do
dep_path="${queue[$index]}"
dep_name="$(basename "$dep_path")"
copy_local_dep "$dep_path"
while read -r child_path; do
[ -n "$child_path" ] || continue
child_path="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$child_path")"
if ! grep -Fxq "$child_path" "$SEEN_FILE"; then
printf '%s\n' "$child_path" >> "$SEEN_FILE"
queue+=("$child_path")
fi
done < <(collect_local_dep_paths "$FRAMEWORKS_DIR/$dep_name")
index=$((index + 1))
done
rm -f "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@executable_path/../Frameworks/$dep_name" "$APP_BIN" || true
done < <(collect_local_dep_paths "$APP_BIN")
shopt -s nullglob
for lib in "$FRAMEWORKS_DIR"/*.dylib; do
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@loader_path/$dep_name" "$lib" || true
done < <(collect_local_dep_paths "$lib")
done
for file in "$APP_BIN" "$FRAMEWORKS_DIR"/*.dylib; do
[ -e "$file" ] || continue
echo "Local dylib linkage for $file"
otool -L "$file" | grep -E 'libunivalue|verge-qt' || true
done
- name: Remove quarantine
run: find . -name "*.app" -type d -exec xattr -cr {} +
- name: Import Apple signing certificate
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_P12_PASSWORD: ${{ secrets.MACOS_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/macos-cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
echo -n "$MACOS_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MACOS_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security default-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | awk -F '\"' '/Developer ID Application:/ { print $2; exit }')"
if [ -z "$SIGNING_IDENTITY" ]; then
echo "::error::No Developer ID Application identity found in imported certificate."
security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true
exit 1
fi
echo "MACOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "MACOS_SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
- name: Codesign app
env:
MACOS_SIGNING_IDENTITY: ${{ env.MACOS_SIGNING_IDENTITY }}
MACOS_KEYCHAIN_PATH: ${{ env.MACOS_KEYCHAIN_PATH }}
run: |
set -euo pipefail
ENTITLEMENTS="$GITHUB_WORKSPACE/contrib/macdeploy/qtwebengine.entitlements"
echo "Signing app with Developer ID identity: $MACOS_SIGNING_IDENTITY"
sign_file() {
local target="$1"
shift
echo "Signing file: $target"
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "$@" "$target"
}
find "Verge-Qt.app" -type f \( \
-name "*.dylib" -o \
-path "*/Contents/MacOS/*" \
\) ! -path "*/_CodeSignature/*" | sort | while read -r TARGET; do
case "$TARGET" in
*/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess|*/Contents/MacOS/verge-qt)
sign_file "$TARGET" --entitlements "$ENTITLEMENTS"
;;
*)
sign_file "$TARGET"
;;
esac
done
find "Verge-Qt.app" -depth \( \
-type d -name "*.framework" -o \
-type d -name "*.app" \
\) | while read -r BUNDLE; do
case "$BUNDLE" in
"Verge-Qt.app")
;;
*)
echo "Signing bundle: $BUNDLE"
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "$BUNDLE"
;;
esac
done
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "Verge-Qt.app"
- name: Create DMG
run: hdiutil create Verge-Qt.dmg -srcfolder "Verge-Qt.app" -ov -format UDZO
- name: Sign DMG
env:
MACOS_SIGNING_IDENTITY: ${{ env.MACOS_SIGNING_IDENTITY }}
MACOS_KEYCHAIN_PATH: ${{ env.MACOS_KEYCHAIN_PATH }}
run: |
set -euo pipefail
codesign --force --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "Verge-Qt.dmg"
- name: Test app launch from DMG
run: |
set -euo pipefail
# Ensure previous UI tests don't leave verge-qt running.
pkill -f '/verge-qt' || true
MOUNT_OUTPUT="$(hdiutil attach "Verge-Qt.dmg" -nobrowse -readonly)"
MOUNT_DEVICE="$(echo "$MOUNT_OUTPUT" | awk '/^\/dev\// {print $1; exit}')"
MOUNT_POINT="$(echo "$MOUNT_OUTPUT" | awk '/\/Volumes\// {print substr($0, index($0, "/Volumes/")); exit}')"
if [ -z "${MOUNT_POINT:-}" ] || [ -z "${MOUNT_DEVICE:-}" ]; then
echo "::error::Failed to detect DMG mount point"
echo "$MOUNT_OUTPUT"
exit 1
fi
detach_with_timeout() {
local target="$1"
hdiutil detach "$target" -force >/dev/null 2>&1 &
local detach_pid=$!
local waited=0
while kill -0 "$detach_pid" 2>/dev/null; do
if [ "$waited" -ge 20 ]; then
echo "::warning::Timed out detaching $target; continuing"
kill -9 "$detach_pid" 2>/dev/null || true
return 0
fi
sleep 1
waited=$((waited + 1))
done
wait "$detach_pid" || true
}
cleanup() {
# Stop any app instance launched from the mounted DMG.
pkill -f "$MOUNT_POINT/Verge-Qt.app/Contents/MacOS/verge-qt" || true
pkill -f '/verge-qt' || true
pkill -9 -f '/verge-qt' || true
sleep 1
detach_with_timeout "$MOUNT_DEVICE"
}
trap cleanup EXIT
APP_BIN="$MOUNT_POINT/Verge-Qt.app/Contents/MacOS/verge-qt"
APP_FRAMEWORKS="$MOUNT_POINT/Verge-Qt.app/Contents/Frameworks"
if [ ! -x "$APP_BIN" ]; then
echo "::error::App binary not found in DMG at $APP_BIN"
ls -la "$MOUNT_POINT"
exit 1
fi
DYLD_FRAMEWORK_PATH="$APP_FRAMEWORKS:${DYLD_FRAMEWORK_PATH:-}" \
DYLD_LIBRARY_PATH="$APP_FRAMEWORKS:${DYLD_LIBRARY_PATH:-}" \
"$APP_BIN" &
APP_PID=$!
sleep 8
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "::error::verge-qt failed to stay running from mounted DMG"
wait "$APP_PID" || true
exit 1
fi
kill "$APP_PID" || true
sleep 2
kill -9 "$APP_PID" 2>/dev/null || true
pkill -f '/verge-qt' || true
wait "$APP_PID" || true
- name: Gatekeeper check (informational)
run: |
set +e
spctl --assess --type open --verbose Verge-Qt.dmg
status=$?
if [ "$status" -ne 0 ]; then
echo "::warning::Gatekeeper rejected Verge-Qt.dmg (expected in CI for ad-hoc/non-notarized artifacts)."
fi
exit 0
- uses: actions/upload-artifact@v4
with:
name: verge-macos26-raw
path: Verge-Qt.dmg
if-no-files-found: error
macos14:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: git config credential.helper
run: git config credential.helper
- name: Brew install base dependencies
run: |
# A workaround for "The `brew link` step did not complete successfully" error.
brew install --quiet python@3 || brew link --overwrite python@3
brew install --quiet automake autoconf berkeley-db@4 pkg-config miniupnpc zeromq libtool boost gperf qrencode librsvg openssl@4 libevent protobuf
- name: Brew link dependencies
run: brew link berkeley-db@4 boost
- name: Install Qt 6.10.2 for macOS
run: |
set -euo pipefail
python3 -m venv "$RUNNER_TEMP/aqt-venv"
source "$RUNNER_TEMP/aqt-venv/bin/activate"
python -m pip install --upgrade pip aqtinstall
QT_ROOT="$RUNNER_TEMP/qt"
python -m aqt install-qt mac desktop 6.10.2 clang_64 \
--outputdir "$QT_ROOT" \
-m qtwebengine qtwebchannel qtpositioning qt5compat
QMAKE_PATH="$(find "$QT_ROOT" -type f \( -name qmake6 -o -name qmake \) -path "*/bin/*" | head -n1 || true)"
if [ -z "${QMAKE_PATH:-}" ]; then
echo "ERROR: Failed to resolve qmake under $QT_ROOT"
find "$QT_ROOT" -maxdepth 5 -type d | sed -n '1,200p'
exit 1
fi
QT_PREFIX="$(dirname "$(dirname "$QMAKE_PATH")")"
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
echo "Installed Qt prefix: $QT_PREFIX"
- name: check cellar for openssl
run: cd /opt/homebrew/Cellar/openssl@4/ && ls
- name: which clang/xcode
run: clang --version
- name: Add Qt tools to PATH
run: |
echo "$QT_PREFIX/bin" >> "$GITHUB_PATH"
echo "$QT_PREFIX/libexec" >> "$GITHUB_PATH"
- name: Resolve Qt host tools
run: |
set -euo pipefail
ALIAS_DIR="$PWD/.qt-tool-aliases"
mkdir -p "$ALIAS_DIR"
declare -a QT_TOOL_DIRS=(
"$QT_PREFIX/libexec"
"$QT_PREFIX/bin"
)
QMAKE_BIN="$(command -v qmake6 || command -v qmake || true)"
if [ -n "$QMAKE_BIN" ]; then
for qdir in \
"$("$QMAKE_BIN" -query QT_HOST_BINS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_INSTALL_BINS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_HOST_LIBEXECS 2>/dev/null || true)" \
"$("$QMAKE_BIN" -query QT_INSTALL_LIBEXECS 2>/dev/null || true)"
do
if [ -n "$qdir" ]; then
QT_TOOL_DIRS+=("$qdir")
fi
done
fi
find_qt_tool() {
local base="$1"
shift || true
local names=("$base" "${base}6" "${base}-qt6")
local d n cand
for d in "${QT_TOOL_DIRS[@]}"; do
[ -d "$d" ] || continue
for n in "${names[@]}"; do
cand="$d/$n"
if [ -x "$cand" ]; then
echo "$cand"
return 0
fi
done
done
return 1
}
MOC_REAL="$(find_qt_tool moc || true)"
UIC_REAL="$(find_qt_tool uic || true)"
RCC_REAL="$(find_qt_tool rcc || true)"
if [ -z "$MOC_REAL" ] || [ -z "$UIC_REAL" ] || [ -z "$RCC_REAL" ]; then
echo "ERROR: Could not resolve Qt host tools."
echo "QMAKE_BIN=${QMAKE_BIN:-<none>}"
echo "QT tool dirs searched:"
printf ' - %s\n' "${QT_TOOL_DIRS[@]}"
echo "Listing candidate dirs:"
for d in "${QT_TOOL_DIRS[@]}"; do
if [ -d "$d" ]; then
echo "-- $d"
ls -la "$d" | egrep 'moc|uic|rcc|qmake' || true
fi
done
exit 1
fi
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc"
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc-qt6"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic-qt6"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc-qt6"
echo "$ALIAS_DIR" >> "$GITHUB_PATH"
echo "MOC=$ALIAS_DIR/moc" >> "$GITHUB_ENV"
echo "UIC=$ALIAS_DIR/uic" >> "$GITHUB_ENV"
echo "RCC=$ALIAS_DIR/rcc" >> "$GITHUB_ENV"
- name: Verify Qt tools
run: |
set -euo pipefail
which moc || true
which moc6 || true
ls -l "$PWD/.qt-tool-aliases" || true
ls -l "$QT_PREFIX/bin" | egrep 'moc|moc6|uic|rcc|qmake' || true
ls -l "$QT_PREFIX/libexec" | egrep 'moc|moc6|uic|rcc|qmake' || true
moc -v
- name: Auto generate
run: ./autogen.sh
- name: configure
run: |
set -euo pipefail
BREW_PREFIX="$(brew --prefix)"
BOOST_PREFIX="$(brew --prefix boost)"
BDB_PREFIX="$(brew --prefix berkeley-db@4)"
OPENSSL_PREFIX="$(brew --prefix openssl@4)"
LIBEVENT_PREFIX="$(brew --prefix libevent)"
PROTOBUF_PREFIX="$(brew --prefix protobuf)"
QMAKE_BIN="$(command -v qmake6 || command -v qmake)"
QT_INCLUDE_DIR="$("$QMAKE_BIN" -query QT_INSTALL_HEADERS)"
QT_LIB_DIR="$("$QMAKE_BIN" -query QT_INSTALL_LIBS)"
QT_PLUGIN_DIR="$("$QMAKE_BIN" -query QT_INSTALL_PLUGINS)"
export CPPFLAGS="-I$PROTOBUF_PREFIX/include -I$BOOST_PREFIX/include -I$BDB_PREFIX/include -I$BREW_PREFIX/include ${CPPFLAGS:-}"
export LDFLAGS="-L$PROTOBUF_PREFIX/lib -L$BOOST_PREFIX/lib -L$BDB_PREFIX/lib -L$BREW_PREFIX/lib ${LDFLAGS:-}"
export PKG_CONFIG_PATH="$PROTOBUF_PREFIX/lib/pkgconfig:$QT_LIB_DIR/pkgconfig:$BOOST_PREFIX/lib/pkgconfig:$BDB_PREFIX/lib/pkgconfig:$BREW_PREFIX/lib/pkgconfig:$BREW_PREFIX/share/pkgconfig:${PKG_CONFIG_PATH:-}"
export CXXFLAGS="-std=c++17 -D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION -Wno-deprecated-builtins -Wno-deprecated-declarations"
export OBJCXXFLAGS="$CXXFLAGS"
export CFLAGS="-Wno-deprecated-builtins -Wno-deprecated-declarations"
./configure \
--disable-bench \
--disable-tests \
--disable-dependency-tracking \
--disable-werror \
--with-gui=qt6 \
--with-qt-bindir="$PWD/.qt-tool-aliases" \
--with-qt-incdir="$QT_INCLUDE_DIR" \
--with-qt-libdir="$QT_LIB_DIR" \
--with-qt-plugindir="$QT_PLUGIN_DIR" \
--bindir="$(pwd)/release/bin" \
--libdir="$(pwd)/release/lib" \
--with-openssl-dir="$OPENSSL_PREFIX" \
--with-libevent-dir="$LIBEVENT_PREFIX" \
--with-boost="$BOOST_PREFIX"
- name: make
run: |
export DEPENDS_PREFIX="$PWD/depends/x86_64-linux-gnu"
export CPPFLAGS="-I$DEPENDS_PREFIX/include -I$DEPENDS_PREFIX/include/db4"
export LDFLAGS="-L$DEPENDS_PREFIX/lib"
make -j4
- name: Test verged daemon
run: |
VERGED_BIN="./src/verged"
CLI_BIN="./src/verge-cli"
if [ ! -f "$VERGED_BIN" ]; then
echo "::error::verged not found at $VERGED_BIN"
exit 1
fi
"$VERGED_BIN" -daemon
sleep 5
if ! pgrep -f verged >/dev/null; then
echo "::error::verged failed to start"
exit 1
fi
"$CLI_BIN" getinfo || true
"$CLI_BIN" stop
- name: Test verge-qt directly
run: |
QT_BIN="./src/qt/verge-qt"
export DYLD_FRAMEWORK_PATH="$QT_PREFIX/lib:${DYLD_FRAMEWORK_PATH:-}"
export DYLD_LIBRARY_PATH="$QT_PREFIX/lib:${DYLD_LIBRARY_PATH:-}"
if [ ! -f "$QT_BIN" ]; then
echo "::error::verge-qt not found at $QT_BIN"
exit 1
fi
chmod +x "$QT_BIN"
pkill -f '/verge-qt' || true
"$QT_BIN" &
QT_PID=$!
sleep 5
if ! kill -0 "$QT_PID" 2>/dev/null; then
echo "::error::verge-qt failed to start"
wait "$QT_PID" || true
exit 1
fi
echo "verge-qt started successfully"
kill "$QT_PID" || true
sleep 2
kill -9 "$QT_PID" 2>/dev/null || true
pkill -f '/verge-qt' || true
wait "$QT_PID" || true
- name: Upload verge-qt raw binary
uses: actions/upload-artifact@v4
with:
name: verge-qt-raw-macos14
path: src/qt/verge-qt
if-no-files-found: error
- name: Wrap binary into .app bundle
run: |
resolve_macho_binary() {
local candidate="$1"
if [ -f "$candidate" ] && file "$candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$candidate"
return 0
fi
local libtool_candidate
libtool_candidate="$(find "$(dirname "$candidate")" -path "*/.libs/$(basename "$candidate")" -type f | head -n1)"
if [ -n "$libtool_candidate" ] && file "$libtool_candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$libtool_candidate"
return 0
fi
return 1
}
BINARY="./src/qt/verge-qt"
if [ ! -f "$BINARY" ]; then
BINARY="$(find . -path '*/src/qt/verge-qt' -type f | head -n1)"
fi
if [ -z "$BINARY" ] || [ ! -f "$BINARY" ]; then
echo "::error::verge-qt binary not found after make"
exit 1
fi
MACHO_BINARY="$(resolve_macho_binary "$BINARY" || true)"
if [ -z "$MACHO_BINARY" ]; then
echo "::error::Unable to resolve a Mach-O verge-qt binary from $BINARY"
ls -la "$(dirname "$BINARY")" || true
file "$BINARY" || true
find "$(dirname "$BINARY")" -path '*/.libs/verge-qt' -type f -exec file {} \; || true
exit 1
fi
echo "Found verge-qt launcher at $BINARY"
echo "Using Mach-O binary at $MACHO_BINARY"
APP_NAME="Verge-Qt.app"
mkdir -p "$APP_NAME/Contents/MacOS"
cp -fL "$MACHO_BINARY" "$APP_NAME/Contents/MacOS/verge-qt"
chmod +x "$APP_NAME/Contents/MacOS/verge-qt"
echo '<?xml version="1.0" encoding="UTF-8"?>' > "$APP_NAME/Contents/Info.plist"
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' >> "$APP_NAME/Contents/Info.plist"
echo '<plist version="1.0">' >> "$APP_NAME/Contents/Info.plist"
echo '<dict>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleName</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>Verge</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleIdentifier</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>org.vergefoundation.verge-qt</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleVersion</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>1.0</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundleExecutable</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>verge-qt</string>' >> "$APP_NAME/Contents/Info.plist"
echo ' <key>CFBundlePackageType</key>' >> "$APP_NAME/Contents/Info.plist"
echo ' <string>APPL</string>' >> "$APP_NAME/Contents/Info.plist"
echo '</dict>' >> "$APP_NAME/Contents/Info.plist"
echo '</plist>' >> "$APP_NAME/Contents/Info.plist"
- name: Bundle with macdeployqt
run: |
APP_NAME="Verge-Qt.app"
macdeployqt "$APP_NAME" -verbose=2
- name: Bundle QtWebEngine helper frameworks
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
HELPER_APP="$FRAMEWORKS_DIR/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app"
HELPER_BIN="$HELPER_APP/Contents/MacOS/QtWebEngineProcess"
if [ ! -x "$HELPER_BIN" ]; then
echo "::error::QtWebEngineProcess helper missing at $HELPER_BIN"
exit 1
fi
extract_framework_root() {
python3 -c 'import sys; path=sys.argv[1]; marker=".framework/"; idx=path.find(marker); print(path[:idx+len(".framework")] if idx >= 0 else "")' "$1"
}
framework_binary_name() {
python3 -c 'import os,sys; name=os.path.basename(sys.argv[1]); print(name[:-10] if name.endswith(".framework") else name)' "$1"
}
is_macho_binary() {
file "$1" 2>/dev/null | grep -Eq 'Mach-O'
}
is_homebrew_qt_framework_ref() {
case "$1" in
/opt/homebrew/*/Qt*.framework/*|/usr/local/*/Qt*.framework/*) return 0 ;;
*) return 1 ;;
esac
}
copy_qt_framework() {
local source_framework="$1"
local framework_name
framework_name="$(basename "$source_framework")"
local dest_framework="$FRAMEWORKS_DIR/$framework_name"
if [ -e "$dest_framework" ]; then
return 0
fi
if [ ! -d "$source_framework" ]; then
echo "::error::Missing Qt framework source: $source_framework"
exit 1
fi
cp -R "$source_framework" "$dest_framework"
chmod -R u+w "$dest_framework"
}
map_qt_refs() {
local binary="$1"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
if ! is_homebrew_qt_framework_ref "$dep_path"; then
continue
fi
local framework_root
framework_root="$(extract_framework_root "$dep_path")"
if [ -z "$framework_root" ]; then
echo "::error::Could not determine framework root for $dep_path"
exit 1
fi
local framework_name
framework_name="$(basename "$framework_root")"
copy_qt_framework "$framework_root"
local framework_binary
framework_binary="$(framework_binary_name "$framework_name")"
local new_ref
new_ref="$(python3 -c 'import os,sys; binary=sys.argv[1]; target=sys.argv[2]; print("@loader_path/" + os.path.relpath(target, os.path.dirname(binary)).replace(os.sep, "/"))' "$binary" "$FRAMEWORKS_DIR/$framework_name/Versions/A/$framework_binary")"
install_name_tool -change "$dep_path" "$new_ref" "$binary"
done < <(otool -L "$binary" | awk 'NR > 1 { print $1 }')
}
map_bundle_framework_refs() {
local binary="$1"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
case "$dep_path" in
@executable_path/../Frameworks/*)
local target_path=""
case "$dep_path" in
*@executable_path/../Frameworks/*.framework/*|@executable_path/../Frameworks/*.framework/*)
local framework_root
framework_root="$(extract_framework_root "$dep_path")"
[ -n "$framework_root" ] || continue
local framework_name
framework_name="$(basename "$framework_root")"
local framework_binary
framework_binary="$(framework_binary_name "$framework_name")"
target_path="$FRAMEWORKS_DIR/$framework_name/Versions/A/$framework_binary"
;;
*)
local dep_name
dep_name="$(basename "$dep_path")"
target_path="$FRAMEWORKS_DIR/$dep_name"
;;
esac
[ -e "$target_path" ] || continue
local new_ref
new_ref="$(python3 -c 'import os,sys; binary=sys.argv[1]; target=sys.argv[2]; print("@loader_path/" + os.path.relpath(target, os.path.dirname(binary)).replace(os.sep, "/"))' "$binary" "$target_path")"
install_name_tool -change "$dep_path" "$new_ref" "$binary"
;;
esac
done < <(otool -L "$binary" | awk 'NR > 1 { print $1 }')
}
map_qt_refs "$HELPER_BIN"
map_bundle_framework_refs "$HELPER_BIN"
while read -r bundled_bin; do
[ -n "$bundled_bin" ] || continue
is_macho_binary "$bundled_bin" || continue
map_qt_refs "$bundled_bin"
map_bundle_framework_refs "$bundled_bin"
done < <(find "$FRAMEWORKS_DIR" -type f)
add_rpath_if_missing() {
local binary="$1"
local rpath="$2"
if ! otool -l "$binary" | awk '
$1 == "cmd" && $2 == "LC_RPATH" { in_rpath=1; next }
in_rpath && $1 == "path" { print $2; in_rpath=0 }
' | grep -Fxq "$rpath"; then
install_name_tool -add_rpath "$rpath" "$binary"
fi
}
add_rpath_if_missing "$APP_BIN" "@executable_path/../Frameworks"
add_rpath_if_missing "$HELPER_BIN" "@executable_path/../../../../.."
if otool -L "$HELPER_BIN" | awk 'NR > 1 { print $1 }' | grep -Eq '^(/opt/homebrew|/usr/local)/.*Qt.*\.framework/'; then
echo "::error::QtWebEngineProcess still has Homebrew Qt framework references after bundling"
otool -L "$HELPER_BIN"
exit 1
fi
python3 - <<'PY'
import os
import re
import subprocess
import sys
frameworks_dir = os.path.join("Verge-Qt.app", "Contents", "Frameworks")
bad = []
pattern = re.compile(r'^@executable_path/\.\./Frameworks/')
for root, _, files in os.walk(frameworks_dir):
for name in files:
if "/Versions/A/" not in os.path.join(root, name).replace("\\", "/"):
continue
binary = os.path.join(root, name)
try:
file_output = subprocess.check_output(["file", binary], text=True)
except subprocess.CalledProcessError:
continue
if "Mach-O" not in file_output:
continue
try:
output = subprocess.check_output(["otool", "-L", binary], text=True)
except subprocess.CalledProcessError:
continue
lines = [line.strip() for line in output.splitlines()[1:]]
framework_name = None
if ".framework/Versions/A/" in binary:
framework_name = binary.split(".framework/Versions/A/")[0].split("/")[-1] + ".framework"
for line in lines:
dep = line.split(" ", 1)[0]
if not pattern.match(dep):
continue
if framework_name and dep == f"@executable_path/../Frameworks/{framework_name}/Versions/A/{name}":
continue
bad.append((binary, dep))
if bad:
print("::error::Bundled binaries still reference bundle deps via @executable_path")
for binary, dep in bad:
print(f"-- {binary}")
print(dep)
sys.exit(1)
PY
echo "QtWebEngine helper linkage:"
otool -L "$HELPER_BIN" | grep -E 'Qt(WebEngine|Qml|Quick|Core|Gui|Network|OpenGL)' || true
- name: Bundle transitive Boost dylibs
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
BOOST_LIB_DIR="$(brew --prefix boost)/lib"
mkdir -p "$FRAMEWORKS_DIR"
shopt -s nullglob
rm -f "$FRAMEWORKS_DIR"/libboost_*.dylib
resolve_boost_source() {
local dep_name="$1"
local candidate
for candidate in \
"$BOOST_LIB_DIR/$dep_name" \
"$BOOST_LIB_DIR/$dep_name".*.dylib \
"$BOOST_LIB_DIR/$dep_name"-*.dylib
do
[ -e "$candidate" ] || continue
candidate="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$candidate")"
[ -f "$candidate" ] || continue
if file "$candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$candidate"
return 0
fi
done
{
echo "::error::Unable to locate Boost dependency $dep_name in $BOOST_LIB_DIR"
ls -la "$BOOST_LIB_DIR" | grep libboost || true
} >&2
exit 1
}
copy_boost_dep() {
local dep_name="$1"
local source target
source="$(resolve_boost_source "$dep_name")"
target="$FRAMEWORKS_DIR/$dep_name"
cp -fL "$source" "$target"
chmod u+w "$target"
if ! file "$target" | grep -q 'Mach-O'; then
echo "::error::Copied Boost library is not Mach-O: $target"
ls -l "$target" || true
file "$target" || true
exit 1
fi
install_name_tool -id "@loader_path/$dep_name" "$target"
}
collect_boost_names() {
local file="$1"
otool -L "$file" | awk 'NR > 1 && /libboost_[^[:space:]]+\.dylib/ { n=$1; sub(/^.*\//, "", n); sub(/:$/, "", n); print n }'
}
declare -a queue=()
SEEN_FILE="$RUNNER_TEMP/boost-seen-$$.txt"
: > "$SEEN_FILE"
while read -r dep_name; do
[ -n "$dep_name" ] || continue
if ! grep -Fxq "$dep_name" "$SEEN_FILE"; then
printf '%s\n' "$dep_name" >> "$SEEN_FILE"
queue+=("$dep_name")
fi
done < <(collect_boost_names "$APP_BIN")
if [ "${#queue[@]}" -eq 0 ]; then
echo "::error::No Boost dylibs referenced by $APP_BIN"
otool -L "$APP_BIN" || true
exit 1
fi
index=0
while [ "$index" -lt "${#queue[@]}" ]; do
dep_name="${queue[$index]}"
copy_boost_dep "$dep_name"
while read -r child_name; do
[ -n "$child_name" ] || continue
if ! grep -Fxq "$child_name" "$SEEN_FILE"; then
printf '%s\n' "$child_name" >> "$SEEN_FILE"
queue+=("$child_name")
fi
done < <(collect_boost_names "$FRAMEWORKS_DIR/$dep_name")
index=$((index + 1))
done
rm -f "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@executable_path/../Frameworks/$dep_name" "$APP_BIN" || true
done < <(otool -L "$APP_BIN" | awk '/libboost_[^[:space:]]+\.dylib/ { print $1 }')
for lib in "$FRAMEWORKS_DIR"/libboost_*.dylib; do
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@loader_path/$dep_name" "$lib" || true
done < <(otool -L "$lib" | awk '/libboost_[^[:space:]]+\.dylib/ { print $1 }')
done
for file in "$APP_BIN" "$FRAMEWORKS_DIR"/libboost_*.dylib; do
echo "Boost linkage for $file"
otool -L "$file" | grep -E 'libboost_|verge-qt' || true
done
- name: Bundle locally built dylibs
run: |
set -euo pipefail
APP_NAME="Verge-Qt.app"
APP_BIN="$APP_NAME/Contents/MacOS/verge-qt"
FRAMEWORKS_DIR="$APP_NAME/Contents/Frameworks"
WORKSPACE_ROOT="$(pwd)"
mkdir -p "$FRAMEWORKS_DIR"
collect_local_dep_paths() {
local file="$1"
otool -L "$file" | awk 'NR > 1 { print $1 }' | while read -r dep_path; do
[ -n "$dep_path" ] || continue
case "$dep_path" in
"$WORKSPACE_ROOT"/*.dylib)
printf '%s\n' "$dep_path"
;;
esac
done
}
resolve_local_dep_source() {
local source="$1"
local resolved dep_name candidate
if [ -e "$source" ]; then
resolved="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$source")"
if [ -f "$resolved" ] && file "$resolved" | grep -q 'Mach-O'; then
printf '%s\n' "$resolved"
return 0
fi
fi
dep_name="$(basename "$source")"
while read -r candidate; do
[ -n "$candidate" ] || continue
resolved="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$candidate")"
if [ -f "$resolved" ] && file "$resolved" | grep -q 'Mach-O'; then
printf '%s\n' "$resolved"
return 0
fi
done < <(find "$WORKSPACE_ROOT" \( -path "*/.libs/$dep_name" -o -path "*/$dep_name" \) \( -type f -o -type l \) 2>/dev/null)
return 1
}
copy_local_dep() {
local source="$1"
local dep_name target
source="$(resolve_local_dep_source "$source" || true)"
if [ -z "$source" ] || [ ! -f "$source" ]; then
echo "::error::Missing local dylib dependency at $source"
exit 1
fi
dep_name="$(basename "$source")"
target="$FRAMEWORKS_DIR/$dep_name"
cp -fL "$source" "$target"
chmod u+w "$target"
if ! file "$target" | grep -q 'Mach-O'; then
echo "::error::Copied local dependency is not Mach-O: $target"
ls -l "$target" || true
file "$target" || true
exit 1
fi
install_name_tool -id "@loader_path/$dep_name" "$target"
}
declare -a queue=()
SEEN_FILE="$RUNNER_TEMP/local-dylib-seen-$$.txt"
: > "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_path="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$dep_path")"
if ! grep -Fxq "$dep_path" "$SEEN_FILE"; then
printf '%s\n' "$dep_path" >> "$SEEN_FILE"
queue+=("$dep_path")
fi
done < <(collect_local_dep_paths "$APP_BIN")
index=0
while [ "$index" -lt "${#queue[@]}" ]; do
dep_path="${queue[$index]}"
dep_name="$(basename "$dep_path")"
copy_local_dep "$dep_path"
while read -r child_path; do
[ -n "$child_path" ] || continue
child_path="$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$child_path")"
if ! grep -Fxq "$child_path" "$SEEN_FILE"; then
printf '%s\n' "$child_path" >> "$SEEN_FILE"
queue+=("$child_path")
fi
done < <(collect_local_dep_paths "$FRAMEWORKS_DIR/$dep_name")
index=$((index + 1))
done
rm -f "$SEEN_FILE"
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@executable_path/../Frameworks/$dep_name" "$APP_BIN" || true
done < <(collect_local_dep_paths "$APP_BIN")
shopt -s nullglob
for lib in "$FRAMEWORKS_DIR"/*.dylib; do
while read -r dep_path; do
[ -n "$dep_path" ] || continue
dep_name="$(basename "$dep_path")"
install_name_tool -change "$dep_path" "@loader_path/$dep_name" "$lib" || true
done < <(collect_local_dep_paths "$lib")
done
for file in "$APP_BIN" "$FRAMEWORKS_DIR"/*.dylib; do
[ -e "$file" ] || continue
echo "Local dylib linkage for $file"
otool -L "$file" | grep -E 'libunivalue|verge-qt' || true
done
- name: Remove quarantine
run: find . -name "*.app" -type d -exec xattr -cr {} +
- name: Import Apple signing certificate
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_P12_PASSWORD: ${{ secrets.MACOS_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/macos-cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
echo -n "$MACOS_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MACOS_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security default-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | awk -F '\"' '/Developer ID Application:/ { print $2; exit }')"
if [ -z "$SIGNING_IDENTITY" ]; then
echo "::error::No Developer ID Application identity found in imported certificate."
security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true
exit 1
fi
echo "MACOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "MACOS_SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
- name: Codesign app
env:
MACOS_SIGNING_IDENTITY: ${{ env.MACOS_SIGNING_IDENTITY }}
MACOS_KEYCHAIN_PATH: ${{ env.MACOS_KEYCHAIN_PATH }}
run: |
set -euo pipefail
ENTITLEMENTS="$GITHUB_WORKSPACE/contrib/macdeploy/qtwebengine.entitlements"
echo "Signing app with Developer ID identity: $MACOS_SIGNING_IDENTITY"
sign_file() {
local target="$1"
shift
echo "Signing file: $target"
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "$@" "$target"
}
find "Verge-Qt.app" -type f \( \
-name "*.dylib" -o \
-path "*/Contents/MacOS/*" \
\) ! -path "*/_CodeSignature/*" | sort | while read -r TARGET; do
case "$TARGET" in
*/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess|*/Contents/MacOS/verge-qt)
sign_file "$TARGET" --entitlements "$ENTITLEMENTS"
;;
*)
sign_file "$TARGET"
;;
esac
done
find "Verge-Qt.app" -depth \( \
-type d -name "*.framework" -o \
-type d -name "*.app" \
\) | while read -r BUNDLE; do
case "$BUNDLE" in
"Verge-Qt.app")
;;
*)
echo "Signing bundle: $BUNDLE"
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "$BUNDLE"
;;
esac
done
codesign --force --options runtime --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "Verge-Qt.app"
- name: Create DMG
run: hdiutil create Verge-Qt.dmg -srcfolder "Verge-Qt.app" -ov -format UDZO
- name: Sign DMG
env:
MACOS_SIGNING_IDENTITY: ${{ env.MACOS_SIGNING_IDENTITY }}
MACOS_KEYCHAIN_PATH: ${{ env.MACOS_KEYCHAIN_PATH }}
run: |
set -euo pipefail
codesign --force --timestamp \
--keychain "$MACOS_KEYCHAIN_PATH" \
--sign "$MACOS_SIGNING_IDENTITY" "Verge-Qt.dmg"
- name: Test app launch from DMG
run: |
set -euo pipefail
# Ensure previous UI tests don't leave verge-qt running.
pkill -f '/verge-qt' || true
MOUNT_OUTPUT="$(hdiutil attach "Verge-Qt.dmg" -nobrowse -readonly)"
MOUNT_DEVICE="$(echo "$MOUNT_OUTPUT" | awk '/^\/dev\// {print $1; exit}')"
MOUNT_POINT="$(echo "$MOUNT_OUTPUT" | awk '/\/Volumes\// {print substr($0, index($0, "/Volumes/")); exit}')"
if [ -z "${MOUNT_POINT:-}" ] || [ -z "${MOUNT_DEVICE:-}" ]; then
echo "::error::Failed to detect DMG mount point"
echo "$MOUNT_OUTPUT"
exit 1
fi
detach_with_timeout() {
local target="$1"
hdiutil detach "$target" -force >/dev/null 2>&1 &
local detach_pid=$!
local waited=0
while kill -0 "$detach_pid" 2>/dev/null; do
if [ "$waited" -ge 20 ]; then
echo "::warning::Timed out detaching $target; continuing"
kill -9 "$detach_pid" 2>/dev/null || true
return 0
fi
sleep 1
waited=$((waited + 1))
done
wait "$detach_pid" || true
}
cleanup() {
# Stop any app instance launched from the mounted DMG.
pkill -f "$MOUNT_POINT/Verge-Qt.app/Contents/MacOS/verge-qt" || true
pkill -f '/verge-qt' || true
pkill -9 -f '/verge-qt' || true
sleep 1
detach_with_timeout "$MOUNT_DEVICE"
}
trap cleanup EXIT
APP_BIN="$MOUNT_POINT/Verge-Qt.app/Contents/MacOS/verge-qt"
APP_FRAMEWORKS="$MOUNT_POINT/Verge-Qt.app/Contents/Frameworks"
if [ ! -x "$APP_BIN" ]; then
echo "::error::App binary not found in DMG at $APP_BIN"
ls -la "$MOUNT_POINT"
exit 1
fi
DYLD_FRAMEWORK_PATH="$APP_FRAMEWORKS:${DYLD_FRAMEWORK_PATH:-}" \
DYLD_LIBRARY_PATH="$APP_FRAMEWORKS:${DYLD_LIBRARY_PATH:-}" \
"$APP_BIN" &
APP_PID=$!
sleep 8
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "::error::verge-qt failed to stay running from mounted DMG"
wait "$APP_PID" || true
exit 1
fi
kill "$APP_PID" || true
sleep 2
kill -9 "$APP_PID" 2>/dev/null || true
pkill -f '/verge-qt' || true
wait "$APP_PID" || true
- name: Gatekeeper check (informational)
run: |
set +e
spctl --assess --type open --verbose Verge-Qt.dmg
status=$?
if [ "$status" -ne 0 ]; then
echo "::warning::Gatekeeper rejected Verge-Qt.dmg (expected in CI for ad-hoc/non-notarized artifacts)."
fi
exit 0
- uses: actions/upload-artifact@v4
with:
name: verge-macos14-raw
path: Verge-Qt.dmg
if-no-files-found: error
macos-26-notarize:
runs-on: macos-26
needs:
- macos-26
steps:
- name: Download previous notary state (if present)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: verge-macos26-notary-state
path: notary_state
- name: Download signed macOS artifact
uses: actions/download-artifact@v4
with:
name: verge-macos26-raw
path: notarize_input
- name: Notarize and staple DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]; then
echo "::error::Missing one or more required Apple notarization secrets: APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD"
exit 1
fi
DMG_PATH="$(find notarize_input -name 'Verge-Qt.dmg' -type f | head -n1)"
if [ -z "${DMG_PATH:-}" ]; then
echo "::error::No DMG found to notarize"
exit 1
fi
DMG_SHA256="$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')"
echo "DMG SHA256: $DMG_SHA256"
mkdir -p notary_state
if [ -f "notary_state/dmg_sha256.txt" ] && [ "$(cat notary_state/dmg_sha256.txt)" = "$DMG_SHA256" ] && [ -f "notary_state/notary_submission_id.txt" ]; then
SUBMISSION_ID="$(cat notary_state/notary_submission_id.txt)"
echo "Reusing prior notary submission id for same DMG: $SUBMISSION_ID"
else
rm -f notary_state/notary_submission_id.txt \
notary_state/notarytool-submit.json \
notary_state/notarytool-info.json \
notary_state/notarytool-log.json
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--output-format json > notary_state/notarytool-submit.json
SUBMISSION_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("id",""))' notary_state/notarytool-submit.json)"
if [ -z "${SUBMISSION_ID:-}" ]; then
echo "::error::Failed to parse notarization submission ID."
cat notary_state/notarytool-submit.json
exit 1
fi
fi
echo "$SUBMISSION_ID" > notary_state/notary_submission_id.txt
echo "$DMG_SHA256" > notary_state/dmg_sha256.txt
echo "Notarization submission ID: $SUBMISSION_ID"
set +e
xcrun notarytool wait "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--timeout 90m
WAIT_RC=$?
set -e
xcrun notarytool info "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--output-format json > notary_state/notarytool-info.json || true
STATUS="$(python3 -c 'import json,sys; import pathlib; p=pathlib.Path(sys.argv[1]); print((json.load(open(p)).get("status","") if p.exists() and p.stat().st_size else ""))' notary_state/notarytool-info.json)"
dump_notary_diagnostics() {
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" > notary_state/notarytool-log.json || true
echo "===== notarytool info ====="
cat notary_state/notarytool-info.json 2>/dev/null || true
echo "===== notarytool log ====="
cat notary_state/notarytool-log.json 2>/dev/null || true
MOUNT_DIR="$(mktemp -d /tmp/verge-notary-XXXXXX)"
MOUNT_OUTPUT="$(hdiutil attach "$DMG_PATH" -nobrowse -readonly -mountpoint "$MOUNT_DIR" 2>/dev/null)" || true
APP_BUNDLE="$MOUNT_DIR/Verge-Qt.app"
APP_BIN="$APP_BUNDLE/Contents/MacOS/verge-qt"
HELPER_BIN="$APP_BUNDLE/Contents/Frameworks/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess"
if [ -e "$APP_BIN" ]; then
echo "===== codesign verify app ====="
codesign --verify --verbose=4 "$APP_BIN" || true
echo "===== entitlements app ====="
codesign -d --entitlements :- "$APP_BIN" || true
fi
if [ -e "$HELPER_BIN" ]; then
echo "===== codesign verify helper ====="
codesign --verify --verbose=4 "$HELPER_BIN" || true
echo "===== entitlements helper ====="
codesign -d --entitlements :- "$HELPER_BIN" || true
fi
if [ -n "${MOUNT_OUTPUT:-}" ]; then
hdiutil detach "$MOUNT_DIR" -force >/dev/null 2>&1 || true
fi
rm -rf "$MOUNT_DIR"
}
if [ "$WAIT_RC" -ne 0 ]; then
echo "Notarization failed or timed out. Submission ID: $SUBMISSION_ID"
dump_notary_diagnostics
exit "$WAIT_RC"
fi
if [ "${STATUS:-}" != "Accepted" ]; then
echo "Notarization did not return Accepted status (status=${STATUS:-unknown})."
dump_notary_diagnostics
exit 1
fi
xcrun stapler staple "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"
set +e
spctl --assess --type open --verbose "$DMG_PATH"
SPCTL_RC=$?
set -e
if [ "$SPCTL_RC" -ne 0 ]; then
echo "::warning::spctl assessment rejected the DMG in CI despite successful notarization and stapling."
fi
{
echo "Apple notarization accepted for macos-26."
echo "Submission ID: $SUBMISSION_ID"
echo "Stapled artifact uploaded from this job."
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload notary state
if: always()
uses: actions/upload-artifact@v4
with:
name: verge-macos26-notary-state
path: notary_state/*
if-no-files-found: ignore
- name: Upload notarized macOS artifact
uses: actions/upload-artifact@v4
with:
name: verge-macos26
path: notarize_input/Verge-Qt.dmg
if-no-files-found: error
macos14-notarize:
runs-on: macos-14
needs:
- macos14
steps:
- name: Download previous notary state (if present)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: verge-macos14-notary-state
path: notary_state
- name: Download signed macOS artifact
uses: actions/download-artifact@v4
with:
name: verge-macos14-raw
path: notarize_input
- name: Notarize and staple DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]; then
echo "::error::Missing one or more required Apple notarization secrets: APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD"
exit 1
fi
DMG_PATH="$(find notarize_input -name 'Verge-Qt.dmg' -type f | head -n1)"
if [ -z "${DMG_PATH:-}" ]; then
echo "::error::No DMG found to notarize"
exit 1
fi
DMG_SHA256="$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')"
echo "DMG SHA256: $DMG_SHA256"
mkdir -p notary_state
if [ -f "notary_state/dmg_sha256.txt" ] && [ "$(cat notary_state/dmg_sha256.txt)" = "$DMG_SHA256" ] && [ -f "notary_state/notary_submission_id.txt" ]; then
SUBMISSION_ID="$(cat notary_state/notary_submission_id.txt)"
echo "Reusing prior notary submission id for same DMG: $SUBMISSION_ID"
else
rm -f notary_state/notary_submission_id.txt \
notary_state/notarytool-submit.json \
notary_state/notarytool-info.json \
notary_state/notarytool-log.json
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--output-format json > notary_state/notarytool-submit.json
SUBMISSION_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("id",""))' notary_state/notarytool-submit.json)"
if [ -z "${SUBMISSION_ID:-}" ]; then
echo "::error::Failed to parse notarization submission ID."
cat notary_state/notarytool-submit.json
exit 1
fi
fi
echo "$SUBMISSION_ID" > notary_state/notary_submission_id.txt
echo "$DMG_SHA256" > notary_state/dmg_sha256.txt
echo "Notarization submission ID: $SUBMISSION_ID"
set +e
xcrun notarytool wait "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--timeout 90m
WAIT_RC=$?
set -e
xcrun notarytool info "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--output-format json > notary_state/notarytool-info.json || true
STATUS="$(python3 -c 'import json,sys; import pathlib; p=pathlib.Path(sys.argv[1]); print((json.load(open(p)).get("status","") if p.exists() and p.stat().st_size else ""))' notary_state/notarytool-info.json)"
dump_notary_diagnostics() {
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" > notary_state/notarytool-log.json || true
echo "===== notarytool info ====="
cat notary_state/notarytool-info.json 2>/dev/null || true
echo "===== notarytool log ====="
cat notary_state/notarytool-log.json 2>/dev/null || true
MOUNT_DIR="$(mktemp -d /tmp/verge-notary-XXXXXX)"
MOUNT_OUTPUT="$(hdiutil attach "$DMG_PATH" -nobrowse -readonly -mountpoint "$MOUNT_DIR" 2>/dev/null)" || true
APP_BUNDLE="$MOUNT_DIR/Verge-Qt.app"
APP_BIN="$APP_BUNDLE/Contents/MacOS/verge-qt"
HELPER_BIN="$APP_BUNDLE/Contents/Frameworks/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess"
if [ -e "$APP_BIN" ]; then
echo "===== codesign verify app ====="
codesign --verify --verbose=4 "$APP_BIN" || true
echo "===== entitlements app ====="
codesign -d --entitlements :- "$APP_BIN" || true
fi
if [ -e "$HELPER_BIN" ]; then
echo "===== codesign verify helper ====="
codesign --verify --verbose=4 "$HELPER_BIN" || true
echo "===== entitlements helper ====="
codesign -d --entitlements :- "$HELPER_BIN" || true
fi
if [ -n "${MOUNT_OUTPUT:-}" ]; then
hdiutil detach "$MOUNT_DIR" -force >/dev/null 2>&1 || true
fi
rm -rf "$MOUNT_DIR"
}
if [ "$WAIT_RC" -ne 0 ]; then
echo "Notarization failed or timed out. Submission ID: $SUBMISSION_ID"
dump_notary_diagnostics
exit "$WAIT_RC"
fi
if [ "${STATUS:-}" != "Accepted" ]; then
echo "Notarization did not return Accepted status (status=${STATUS:-unknown})."
dump_notary_diagnostics
exit 1
fi
xcrun stapler staple "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"
set +e
spctl --assess --type open --verbose "$DMG_PATH"
SPCTL_RC=$?
set -e
if [ "$SPCTL_RC" -ne 0 ]; then
echo "::warning::spctl assessment rejected the DMG in CI despite successful notarization and stapling."
fi
{
echo "Apple notarization accepted for macos14."
echo "Submission ID: $SUBMISSION_ID"
echo "Stapled artifact uploaded from this job."
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload notary state
if: always()
uses: actions/upload-artifact@v4
with:
name: verge-macos14-notary-state
path: notary_state/*
if-no-files-found: ignore
- name: Upload notarized macOS artifact
uses: actions/upload-artifact@v4
with:
name: verge-macos14
path: notarize_input/Verge-Qt.dmg
if-no-files-found: error
ubuntu24:
runs-on: ubuntu-24.04
env:
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- uses: actions/checkout@v4
- name: Cache non-Qt depends build artifacts
uses: actions/cache@v4
with:
path: |
depends/built
depends/x86_64-linux-gnu
depends/sources
key: depends-base-${{ runner.os }}-linux64-v5-${{ hashFiles('depends/packages/**', 'depends/hosts/**', 'depends/**.mk', 'depends/Makefile') }}
restore-keys: |
depends-base-${{ runner.os }}-linux64-v5-
- name: Cache ccache
uses: actions/cache@v4
with:
path: ~/.ccache
key: ccache-${{ runner.os }}-linux64-${{ hashFiles('configure.ac', 'Makefile.am', 'src/**', 'build-aux/**') }}
restore-keys: |
ccache-${{ runner.os }}-linux64-
- name: Update apt repos
run: sudo apt-get update
- name: Install base dependencies
run: |
sudo apt-get install -y \
build-essential xutils-dev libtool gperf autotools-dev automake pkg-config \
bsdmainutils libattr1-dev make automake bison byacc cmake curl \
g++-multilib binutils-gold python3 python3-setuptools gyp ccache ninja-build flex nodejs \
libgl1-mesa-dev libegl1-mesa-dev libopengl-dev \
libx11-dev libx11-xcb-dev libxcb1-dev libxcb-render0-dev libxkbcommon-dev libxkbcommon-x11-dev \
libxcb-cursor-dev libxcb-icccm4-dev libxcb-util-dev libxcb-image0-dev libxcb-keysyms1-dev \
libxcb-randr0-dev libxcb-render-util0-dev libxcb-shm0-dev libxcb-sync-dev libxcb-xfixes0-dev \
libxcb-shape0-dev libxcb-xkb-dev \
libboost-all-dev \
libevent-dev libssl-dev zlib1g-dev libseccomp-dev libcap-dev libprotobuf-dev protobuf-compiler \
libqrencode-dev \
qt6-base-dev qt6-base-private-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools \
qt6-webengine-dev qt6-webengine-dev-tools
- name: Enable ccache wrappers
run: |
sudo /usr/sbin/update-ccache-symlinks || true
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
- name: Configure ccache
run: |
mkdir -p ~/.ccache
echo 'max_size = 2G' > ~/.ccache/ccache.conf
echo 'compression = true' >> ~/.ccache/ccache.conf
ccache -z || true
- name: Prepare Qt6 host tool aliases
run: |
set -euo pipefail
QT_HOST_BINS="$(qtpaths6 --query QT_HOST_BINS)"
QT_HOST_LIBEXECS="$(qtpaths6 --query QT_HOST_LIBEXECS || true)"
QT_INSTALL_LIBEXECS="$(qtpaths6 --query QT_INSTALL_LIBEXECS || true)"
ALIAS_DIR="$PWD/.qt-tool-aliases"
mkdir -p "$ALIAS_DIR"
resolve_qt_tool() {
local tool="$1"
shift
local candidate=""
for candidate in "$@"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
MOC_REAL="$(resolve_qt_tool moc \
"$(command -v moc-qt6 || true)" \
"$(command -v moc6 || true)" \
"$(command -v moc || true)" \
"$QT_HOST_BINS/moc-qt6" \
"$QT_HOST_BINS/moc6" \
"$QT_HOST_BINS/moc" \
"$QT_HOST_LIBEXECS/moc-qt6" \
"$QT_HOST_LIBEXECS/moc6" \
"$QT_HOST_LIBEXECS/moc" \
"$QT_INSTALL_LIBEXECS/moc-qt6" \
"$QT_INSTALL_LIBEXECS/moc6" \
"$QT_INSTALL_LIBEXECS/moc" \
"/usr/lib/qt6/libexec/moc")"
UIC_REAL="$(resolve_qt_tool uic \
"$(command -v uic-qt6 || true)" \
"$(command -v uic6 || true)" \
"$(command -v uic || true)" \
"$QT_HOST_BINS/uic-qt6" \
"$QT_HOST_BINS/uic6" \
"$QT_HOST_BINS/uic" \
"$QT_HOST_LIBEXECS/uic-qt6" \
"$QT_HOST_LIBEXECS/uic6" \
"$QT_HOST_LIBEXECS/uic" \
"$QT_INSTALL_LIBEXECS/uic-qt6" \
"$QT_INSTALL_LIBEXECS/uic6" \
"$QT_INSTALL_LIBEXECS/uic" \
"/usr/lib/qt6/libexec/uic")"
RCC_REAL="$(resolve_qt_tool rcc \
"$(command -v rcc-qt6 || true)" \
"$(command -v rcc6 || true)" \
"$(command -v rcc || true)" \
"$QT_HOST_BINS/rcc-qt6" \
"$QT_HOST_BINS/rcc6" \
"$QT_HOST_BINS/rcc" \
"$QT_HOST_LIBEXECS/rcc-qt6" \
"$QT_HOST_LIBEXECS/rcc6" \
"$QT_HOST_LIBEXECS/rcc" \
"$QT_INSTALL_LIBEXECS/rcc-qt6" \
"$QT_INSTALL_LIBEXECS/rcc6" \
"$QT_INSTALL_LIBEXECS/rcc" \
"/usr/lib/qt6/libexec/rcc")"
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc"
ln -sf "$MOC_REAL" "$ALIAS_DIR/moc-qt6"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic"
ln -sf "$UIC_REAL" "$ALIAS_DIR/uic-qt6"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc"
ln -sf "$RCC_REAL" "$ALIAS_DIR/rcc-qt6"
echo "$ALIAS_DIR" >> "$GITHUB_PATH"
echo "MOC=$ALIAS_DIR/moc" >> "$GITHUB_ENV"
echo "UIC=$ALIAS_DIR/uic" >> "$GITHUB_ENV"
echo "RCC=$ALIAS_DIR/rcc" >> "$GITHUB_ENV"
- name: Sanity-check cached non-Qt depends
run: |
set -euo pipefail
missing=0
for path in \
depends/x86_64-linux-gnu/include/db4/db_cxx.h \
depends/x86_64-linux-gnu/lib/libdb_cxx-4.8.a \
depends/x86_64-linux-gnu/share/config.site
do
if [ ! -e "$path" ]; then
echo "::warning::Missing cached depends artifact $path"
missing=1
fi
done
if [ "$missing" -eq 1 ]; then
echo "::warning::Clearing stale non-Qt depends cache state so required packages rebuild."
rm -rf depends/built depends/x86_64-linux-gnu
fi
- name: Build non-Qt depends
run: |
cd depends/
make -j4 HOST=x86_64-linux-gnu NO_QT=1 install
- name: Auto generate
run: ./autogen.sh
- name: configure
run: |
export DEPENDS_PREFIX="$PWD/depends/x86_64-linux-gnu"
export SYSTEM_QT_PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/qt6/plugins/platforms"
export CPPFLAGS="-I$DEPENDS_PREFIX/include -I$DEPENDS_PREFIX/include/db4"
export LDFLAGS="-L$DEPENDS_PREFIX/lib -L$SYSTEM_QT_PLUGIN_DIR"
export PKG_CONFIG_PATH="$DEPENDS_PREFIX/lib/pkgconfig:$DEPENDS_PREFIX/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
export BDB_CFLAGS="-I$DEPENDS_PREFIX/include/db4"
export BDB_LIBS="-L$DEPENDS_PREFIX/lib -ldb_cxx-4.8"
export LIBCAP_LIBS="-lcap"
CONFIG_SITE=$PWD/depends/x86_64-linux-gnu/share/config.site \
./configure --enable-scrypt-sse2 --disable-bench --disable-tests \
--disable-dependency-tracking --disable-werror \
--with-gui=qt6 \
--with-qrencode \
--with-qt-bindir="$PWD/.qt-tool-aliases" \
--prefix="$(pwd)/depends/x86_64-linux-gnu" \
--bindir="$(pwd)/release/bin" \
--libdir="$(pwd)/release/lib"
- name: make
run: |
export DEPENDS_PREFIX="$PWD/depends/x86_64-linux-gnu"
export SYSTEM_QT_PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/qt6/plugins/platforms"
export CPPFLAGS="-I$DEPENDS_PREFIX/include -I$DEPENDS_PREFIX/include/db4"
export LDFLAGS="-L$DEPENDS_PREFIX/lib -L$SYSTEM_QT_PLUGIN_DIR"
export LIBCAP_LIBS="-lcap"
echo "UIC=$UIC"
"$UIC" --version
"$UIC" -o /tmp/ui_addressbookpage.h src/qt/forms/addressbookpage.ui
"$UIC" -o /tmp/ui_messagepage.h src/qt/forms/messagepage.ui
make -j4 V=1 CPPFLAGS="$CPPFLAGS" LDFLAGS="$LDFLAGS"
- name: strip
run: |
cd ./src && strip .libs/verged .libs/verge-cli .libs/verge-tx
cd ./qt && strip .libs/verge-qt
- name: Build Qt AppImage
run: |
set -euo pipefail
if ! pkg-config --exists Qt6WebEngineWidgets; then
echo "::error::Qt6WebEngineWidgets is required to build the Verge-Qt AppImage"
exit 1
fi
mkdir -p "$PWD/release"
curl -L -o "$PWD/linuxdeploy-x86_64.AppImage" \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
curl -L -o "$PWD/linuxdeploy-plugin-qt-x86_64.AppImage" \
https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x "$PWD/linuxdeploy-x86_64.AppImage" "$PWD/linuxdeploy-plugin-qt-x86_64.AppImage"
rm -rf "$PWD/linuxdeploy-root" "$PWD/linuxdeploy-plugin-qt-root" "$PWD/AppDir-verge-qt"
"$PWD/linuxdeploy-x86_64.AppImage" --appimage-extract >/dev/null
mv "$PWD/squashfs-root" "$PWD/linuxdeploy-root"
"$PWD/linuxdeploy-plugin-qt-x86_64.AppImage" --appimage-extract >/dev/null
mv "$PWD/squashfs-root" "$PWD/linuxdeploy-plugin-qt-root"
export PATH="$PWD/linuxdeploy-plugin-qt-root/usr/bin:$PATH"
export QMAKE="$(command -v qmake6 || command -v qmake)"
export VERSION="${GITHUB_SHA::7}"
APPDIR="$PWD/AppDir-verge-qt"
QT_BIN="$PWD/src/qt/.libs/verge-qt"
UNIVALUE_LIBDIR="$PWD/src/univalue/.libs"
if [ ! -f "$QT_BIN" ]; then
echo "::error::Expected ELF Qt binary not found at $QT_BIN"
exit 1
fi
if ! find "$UNIVALUE_LIBDIR" -maxdepth 1 -type f -name 'libunivalue.so*' | grep -q .; then
echo "::error::Expected local libunivalue shared library not found under $UNIVALUE_LIBDIR"
exit 1
fi
export LD_LIBRARY_PATH="$UNIVALUE_LIBDIR:${LD_LIBRARY_PATH:-}"
mkdir -p "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps"
cat > "$APPDIR/usr/share/applications/verge-qt.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Verge Qt
Exec=verge-qt
Icon=verge
Categories=Network;Finance;
Terminal=false
StartupWMClass=verge-qt
EOF
cp "$PWD/share/pixmaps/verge256.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/verge.png"
cp "$PWD/share/pixmaps/verge256.png" "$APPDIR/verge.png"
"$PWD/linuxdeploy-root/AppRun" \
--appdir "$APPDIR" \
--plugin qt \
--executable "$QT_BIN" \
--desktop-file "$APPDIR/usr/share/applications/verge-qt.desktop" \
--icon-file "$PWD/share/pixmaps/verge256.png"
QT_PLUGINS="$(qtpaths6 --query QT_INSTALL_PLUGINS)"
QT_LIBEXECS="$(qtpaths6 --query QT_INSTALL_LIBEXECS)"
QT_DATA="$(qtpaths6 --query QT_INSTALL_DATA)"
QT_TRANSLATIONS="$(qtpaths6 --query QT_INSTALL_TRANSLATIONS)"
OPENSSL_LIBDIR="$(pkg-config --variable=libdir libssl)"
TLS_PLUGIN="$APPDIR/usr/plugins/tls/libqopensslbackend.so"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/resources" "$APPDIR/usr/translations" "$APPDIR/usr/etc/fonts" "$APPDIR/usr/ssl"
cp "$QT_LIBEXECS/QtWebEngineProcess" "$APPDIR/usr/bin/QtWebEngineProcess"
chmod +x "$APPDIR/usr/bin/QtWebEngineProcess"
for res in icudtl.dat v8_context_snapshot.bin qtwebengine_devtools_resources.pak; do
if [ -f "$QT_DATA/resources/$res" ]; then
cp "$QT_DATA/resources/$res" "$APPDIR/usr/resources/"
fi
done
if ls "$QT_DATA/resources/qtwebengine_resources"*.pak >/dev/null 2>&1; then
cp "$QT_DATA/resources/qtwebengine_resources"*.pak "$APPDIR/usr/resources/"
fi
if [ -d "$QT_DATA/resources/locales" ]; then
cp -a "$QT_DATA/resources/locales" "$APPDIR/usr/resources/"
fi
if [ -d "$QT_TRANSLATIONS/qtwebengine_locales" ]; then
cp -a "$QT_TRANSLATIONS/qtwebengine_locales" "$APPDIR/usr/translations/"
fi
if [ -d /etc/fonts ]; then
cp -a /etc/fonts/. "$APPDIR/usr/etc/fonts/"
fi
if [ -f /etc/ssl/certs/ca-certificates.crt ]; then
cp /etc/ssl/certs/ca-certificates.crt "$APPDIR/usr/ssl/ca-certificates.crt"
fi
cp -a "$UNIVALUE_LIBDIR"/libunivalue.so* "$APPDIR/usr/lib/"
find "$OPENSSL_LIBDIR" -maxdepth 1 \( -type f -o -type l \) \
\( -name 'libssl.so*' -o -name 'libcrypto.so*' \) \
-exec cp -a {} "$APPDIR/usr/lib/" \;
rm -f "$APPDIR/AppRun"
rm -f "$APPDIR/usr/bin/verge-qt"
rm -f "$APPDIR/usr/bin/usr/bin/verge-qt" || true
install -m755 "$QT_BIN" "$APPDIR/usr/bin/verge-qt"
patchelf --set-rpath '$ORIGIN/../lib' "$APPDIR/usr/bin/verge-qt"
patchelf --set-rpath '$ORIGIN/../lib' "$APPDIR/usr/bin/QtWebEngineProcess"
if [ -f "$TLS_PLUGIN" ]; then
patchelf --set-rpath '$ORIGIN/../../lib' "$TLS_PLUGIN"
fi
cat > "$APPDIR/usr/bin/qt.conf" <<EOF
[Paths]
Prefix=..
Plugins=plugins
Libraries=lib
LibraryExecutables=bin
Data=.
Translations=translations
EOF
for required in "$APPDIR/usr/lib/libQt6Core.so.6" "$APPDIR/usr/lib/libQt6Gui.so.6" "$APPDIR/usr/lib/libQt6Network.so.6" "$APPDIR/usr/lib/libQt6Widgets.so.6" "$APPDIR/usr/lib/libQt6WebEngineCore.so.6" "$APPDIR/usr/lib/libQt6WebEngineWidgets.so.6" "$APPDIR/usr/plugins/platforms/libqxcb.so" "$APPDIR/usr/bin/QtWebEngineProcess"; do
if [ ! -e "$required" ]; then
echo "::error::Missing required AppImage runtime file: $required"
exit 1
fi
done
if ! find "$APPDIR/usr/lib" -maxdepth 1 \( -type f -o -type l \) -name 'libssl.so*' | grep -q .; then
echo "::error::Missing bundled OpenSSL libssl in AppImage"
exit 1
fi
if ! find "$APPDIR/usr/lib" -maxdepth 1 \( -type f -o -type l \) -name 'libcrypto.so*' | grep -q .; then
echo "::error::Missing bundled OpenSSL libcrypto in AppImage"
exit 1
fi
if [ -f "$TLS_PLUGIN" ]; then
echo "Qt TLS backend linkage:"
ldd "$TLS_PLUGIN" || true
if ldd "$TLS_PLUGIN" | grep -E 'lib(ssl|crypto)[.]so' | grep -Fv "$APPDIR/" | grep -q .; then
echo "::error::Qt TLS backend still resolves against host OpenSSL"
exit 1
fi
fi
cat > "$APPDIR/usr/ssl/openssl.cnf" <<EOF
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
[default_sect]
activate = 1
EOF
cat > "$APPDIR/AppRun" <<EOF
#!/bin/sh
HERE=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)
export PATH="\$HERE/usr/bin:\$PATH"
export LD_LIBRARY_PATH="\$HERE/usr/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
export QT_PLUGIN_PATH="\$HERE/usr/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="\$HERE/usr/plugins/platforms"
export QML2_IMPORT_PATH="\$HERE/usr/qml"
export XDG_DATA_DIRS="\$HERE/usr/share\${XDG_DATA_DIRS:+:\$XDG_DATA_DIRS}"
export FONTCONFIG_PATH="\$HERE/usr/etc/fonts"
export FONTCONFIG_FILE="\$HERE/usr/etc/fonts/fonts.conf"
export OPENSSL_CONF="\$HERE/usr/ssl/openssl.cnf"
export SSL_CERT_FILE="\$HERE/usr/ssl/ca-certificates.crt"
export SSL_CERT_DIR="/etc/ssl/certs"
export QTWEBENGINEPROCESS_PATH="\$HERE/usr/bin/QtWebEngineProcess"
export QTWEBENGINE_RESOURCES_PATH="\$HERE/usr/resources"
export QTWEBENGINE_LOCALES_PATH="\$HERE/usr/translations/qtwebengine_locales"
export QTWEBENGINE_DISABLE_SANDBOX=1
export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
exec "\$HERE/usr/bin/verge-qt" "\$@"
EOF
chmod +x "$APPDIR/AppRun"
"$PWD/linuxdeploy-root/plugins/linuxdeploy-plugin-appimage/usr/bin/appimagetool" "$APPDIR"
GENERATED_APPIMAGE="$(find "$PWD" -maxdepth 1 -type f -name 'Verge_Qt-*-x86_64.AppImage' -print -quit)"
if [ -z "$GENERATED_APPIMAGE" ]; then
echo "::error::Generated Verge Qt AppImage not found"
exit 1
fi
mv "$GENERATED_APPIMAGE" "$PWD/release/Verge-Qt-x86_64.AppImage"
build_cli_appimage() {
local binary_path="$1"
local app_name="$2"
local exec_name="$3"
local output_name="$4"
local terminal_flag="$5"
local appdir="$PWD/AppDir-$exec_name"
rm -rf "$appdir"
mkdir -p "$appdir/usr/bin" "$appdir/usr/lib" "$appdir/usr/share/applications" "$appdir/usr/share/icons/hicolor/256x256/apps"
install -m755 "$binary_path" "$appdir/usr/bin/$exec_name"
cp -a "$UNIVALUE_LIBDIR"/libunivalue.so* "$appdir/usr/lib/"
patchelf --set-rpath '$ORIGIN/../lib' "$appdir/usr/bin/$exec_name"
cat > "$appdir/usr/share/applications/$exec_name.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=$app_name
Exec=$exec_name
Icon=verge
Categories=Network;Finance;
Terminal=$terminal_flag
EOF
cp "$appdir/usr/share/applications/$exec_name.desktop" "$appdir/$exec_name.desktop"
cp "$PWD/share/pixmaps/verge256.png" "$appdir/usr/share/icons/hicolor/256x256/apps/verge.png"
cp "$PWD/share/pixmaps/verge256.png" "$appdir/verge.png"
cat > "$appdir/AppRun" <<EOF
#!/bin/sh
HERE=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)
exec "\$HERE/usr/bin/$exec_name" "\$@"
EOF
chmod +x "$appdir/AppRun"
export ARCH=x86_64
export LDAI_OUTPUT="$PWD/release/$output_name"
rm -f "$PWD/release/$output_name"
"$PWD/linuxdeploy-root/plugins/linuxdeploy-plugin-appimage/usr/bin/appimagetool" "$appdir"
generated_appimage="$(find "$PWD" -maxdepth 1 -type f -name '*.AppImage' ! -name 'linuxdeploy*.AppImage' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print $2}')"
if [ -z "$generated_appimage" ]; then
echo "::error::Generated AppImage not found for $exec_name"
exit 1
fi
mv "$generated_appimage" "$PWD/release/$output_name"
}
build_cli_appimage "$PWD/src/.libs/verged" "Verge Daemon" "verged" "verged-x86_64.AppImage" "false"
build_cli_appimage "$PWD/src/.libs/verge-cli" "Verge CLI" "verge-cli" "verge-cli-x86_64.AppImage" "true"
build_cli_appimage "$PWD/src/.libs/verge-tx" "Verge TX" "verge-tx" "verge-tx-x86_64.AppImage" "true"
(
cd "$PWD/release"
zip -j verge-ubuntu24-appimages.zip \
Verge-Qt-x86_64.AppImage \
verged-x86_64.AppImage \
verge-cli-x86_64.AppImage \
verge-tx-x86_64.AppImage
)
- name: Test verge-qt AppImage launch
run: |
set -euo pipefail
APP_BIN="$PWD/release/Verge-Qt-x86_64.AppImage"
export QTWEBENGINE_DISABLE_SANDBOX=1
export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
if [ ! -x "$APP_BIN" ]; then
echo "::error::Verge-Qt AppImage not found at $APP_BIN"
exit 1
fi
xvfb-run -a "$APP_BIN" &
APP_PID=$!
sleep 10
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "::error::Verge-Qt AppImage exited early"
wait "$APP_PID"
exit 1
fi
pkill -9 -f 'Verge-Qt-x86_64.AppImage' || true
pkill -9 -f '/tmp/.mount_Verge-' || true
pkill -9 Xvfb || true
sleep 2
kill -9 "$APP_PID" 2>/dev/null || true
wait "$APP_PID" || true
- name: ccache stats
env:
CCACHE_DIR: ~/.ccache
run: ccache -s || true
- uses: actions/upload-artifact@v4
with:
name: verge-ubuntu24
path: |
./release/Verge-Qt-x86_64.AppImage
./release/verged-x86_64.AppImage
./release/verge-cli-x86_64.AppImage
./release/verge-tx-x86_64.AppImage
raspberrypi:
runs-on: ubuntu-24.04-arm
env:
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- uses: actions/checkout@v4
- name: Update apt repos
run: sudo apt-get update
- name: Install base dependencies
run: |
sudo apt-get install -y \
libseccomp-dev git build-essential xutils-dev libtool gperf autotools-dev \
automake pkg-config bsdmainutils libattr1-dev make automake bison byacc \
cmake curl bison byacc python3 libcap-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
ninja-build flex nodejs gyp libgl1-mesa-dev libegl1-mesa-dev libopengl-dev
- name: Export toolchain env
run: |
echo "CC=gcc" >> "$GITHUB_ENV"
echo "CXX=g++" >> "$GITHUB_ENV"
echo "AR=ar" >> "$GITHUB_ENV"
echo "RANLIB=ranlib" >> "$GITHUB_ENV"
echo "STRIP=strip" >> "$GITHUB_ENV"
- name: Build depends
run: |
cd depends/
make -j1 HOST=aarch64-linux-gnu QT_SKIP_WEBENGINE=1 QT_SKIP_QTDECLARATIVE=1 QT_DISABLE_OPENSSL=1
- name: Auto generate
run: ./autogen.sh
- name: configure
run: |
CONFIG_SITE=$PWD/depends/aarch64-linux-gnu/share/config.site \
LIBCAP_LIBS="-lcap" \
./configure \
--host=aarch64-linux-gnu \
--build=aarch64-linux-gnu \
--disable-bench \
--disable-tests \
--disable-dependency-tracking \
--disable-werror \
--with-qrencode \
--prefix="$(pwd)/depends/aarch64-linux-gnu" \
--bindir="$(pwd)/release/bin" \
--libdir="$(pwd)/release/lib"
- name: make
run: make -j4
- name: strip
run: |
cd ./src && strip .libs/verged .libs/verge-cli .libs/verge-tx
cd ./qt && strip .libs/verge-qt
- name: Build Qt AppImage (ARM64)
run: |
set -euo pipefail
mkdir -p "$PWD/release"
curl -L -o "$PWD/linuxdeploy-aarch64.AppImage" \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
chmod +x "$PWD/linuxdeploy-aarch64.AppImage"
rm -rf "$PWD/linuxdeploy-root" "$PWD/AppDir-verge-qt"
"$PWD/linuxdeploy-aarch64.AppImage" --appimage-extract >/dev/null
mv "$PWD/squashfs-root" "$PWD/linuxdeploy-root"
export VERSION="${GITHUB_SHA::7}"
APPDIR="$PWD/AppDir-verge-qt"
QT_BIN="$PWD/src/qt/.libs/verge-qt"
UNIVALUE_LIBDIR="$PWD/src/univalue/.libs"
QT_TARGET_PREFIX="$PWD/depends/aarch64-linux-gnu"
QT_TARGET_LIBDIR="$QT_TARGET_PREFIX/lib"
if [ ! -f "$QT_BIN" ]; then
echo "::error::Expected ELF Qt binary not found at $QT_BIN"
exit 1
fi
if ! find "$UNIVALUE_LIBDIR" -maxdepth 1 -type f -name 'libunivalue.so*' | grep -q .; then
echo "::error::Expected local libunivalue shared library not found under $UNIVALUE_LIBDIR"
exit 1
fi
export LD_LIBRARY_PATH="$QT_TARGET_LIBDIR:$UNIVALUE_LIBDIR:${LD_LIBRARY_PATH:-}"
mkdir -p "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps"
cat > "$APPDIR/usr/share/applications/verge-qt.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Verge Qt
Exec=verge-qt
Icon=verge
Categories=Network;Finance;
Terminal=false
StartupWMClass=verge-qt
EOF
cp "$PWD/share/pixmaps/verge256.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/verge.png"
cp "$PWD/share/pixmaps/verge256.png" "$APPDIR/verge.png"
"$PWD/linuxdeploy-root/AppRun" \
--appdir "$APPDIR" \
--executable "$QT_BIN" \
--desktop-file "$APPDIR/usr/share/applications/verge-qt.desktop" \
--icon-file "$PWD/share/pixmaps/verge256.png"
QT_PLATFORM_PLUGIN="$(find "$QT_TARGET_PREFIX" -path '*/plugins/platforms/libqxcb.so' -type f | head -n1 || true)"
if [ -z "$QT_PLATFORM_PLUGIN" ]; then
echo "::error::Target Qt platform plugin libqxcb.so not found under $QT_TARGET_PREFIX"
find "$QT_TARGET_PREFIX" -path '*/plugins/platforms' -type d -print
exit 1
fi
mkdir -p "$APPDIR/usr/plugins/platforms"
cp "$QT_PLATFORM_PLUGIN" "$APPDIR/usr/plugins/platforms/libqxcb.so"
QT_XCB_QPA_LIB="$(find "$QT_TARGET_PREFIX" -path '*/libQt6XcbQpa.so.6' \( -type f -o -type l \) | head -n1 || true)"
if [ -z "$QT_XCB_QPA_LIB" ]; then
echo "::error::Target Qt XcbQpa library libQt6XcbQpa.so.6 not found under $QT_TARGET_PREFIX"
find "$QT_TARGET_PREFIX" -path '*/libQt6XcbQpa.so*' -print
exit 1
fi
cp "$(readlink -f "$QT_XCB_QPA_LIB")" "$APPDIR/usr/lib/libQt6XcbQpa.so.6"
"$PWD/linuxdeploy-root/AppRun" \
--appdir "$APPDIR" \
--library "$APPDIR/usr/lib/libQt6XcbQpa.so.6"
"$PWD/linuxdeploy-root/AppRun" \
--appdir "$APPDIR" \
--library "$APPDIR/usr/plugins/platforms/libqxcb.so"
cp -a "$UNIVALUE_LIBDIR"/libunivalue.so* "$APPDIR/usr/lib/"
for required in \
"$APPDIR/usr/bin/verge-qt" \
"$APPDIR/usr/share/applications/verge-qt.desktop" \
"$APPDIR/verge.png" \
"$APPDIR/usr/lib/libQt6XcbQpa.so.6" \
"$APPDIR/usr/plugins/platforms/libqxcb.so"
do
if [ ! -e "$required" ]; then
echo "::error::Missing required AppImage runtime file: $required"
exit 1
fi
done
cat > "$APPDIR/usr/bin/qt.conf" <<EOF
[Paths]
Plugins=../plugins
EOF
cat > "$APPDIR/AppRun" <<EOF
#!/bin/sh
HERE=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)
exec "\$HERE/usr/bin/verge-qt" "\$@"
EOF
chmod +x "$APPDIR/AppRun"
export ARCH=aarch64
export LDAI_OUTPUT="$PWD/release/Verge-Qt-aarch64.AppImage"
rm -f "$PWD/release/Verge-Qt-aarch64.AppImage"
"$PWD/linuxdeploy-root/plugins/linuxdeploy-plugin-appimage/usr/bin/appimagetool" "$APPDIR"
GENERATED_APPIMAGE="$(find "$PWD" -maxdepth 1 -type f -name 'Verge_Qt-*-aarch64.AppImage' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print $2}')"
if [ -z "$GENERATED_APPIMAGE" ]; then
echo "::error::Generated Verge Qt ARM64 AppImage not found"
ls -lah "$PWD" | grep AppImage || true
ls -lah "$PWD/release" || true
exit 1
fi
mv "$GENERATED_APPIMAGE" "$PWD/release/Verge-Qt-aarch64.AppImage"
build_cli_appimage() {
local binary_path="$1"
local app_name="$2"
local exec_name="$3"
local output_name="$4"
local terminal_flag="$5"
local appdir="$PWD/AppDir-$exec_name"
rm -rf "$appdir"
mkdir -p "$appdir/usr/bin" "$appdir/usr/lib" "$appdir/usr/share/applications" "$appdir/usr/share/icons/hicolor/256x256/apps"
install -m755 "$binary_path" "$appdir/usr/bin/$exec_name"
cp -a "$UNIVALUE_LIBDIR"/libunivalue.so* "$appdir/usr/lib/"
patchelf --set-rpath '$ORIGIN/../lib' "$appdir/usr/bin/$exec_name"
cat > "$appdir/usr/share/applications/$exec_name.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=$app_name
Exec=$exec_name
Icon=verge
Categories=Network;Finance;
Terminal=$terminal_flag
EOF
cp "$appdir/usr/share/applications/$exec_name.desktop" "$appdir/$exec_name.desktop"
cp "$PWD/share/pixmaps/verge256.png" "$appdir/usr/share/icons/hicolor/256x256/apps/verge.png"
cp "$PWD/share/pixmaps/verge256.png" "$appdir/verge.png"
cat > "$appdir/AppRun" <<EOF
#!/bin/sh
HERE=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)
exec "\$HERE/usr/bin/$exec_name" "\$@"
EOF
chmod +x "$appdir/AppRun"
export ARCH=aarch64
export LDAI_OUTPUT="$PWD/release/$output_name"
rm -f "$PWD/release/$output_name"
"$PWD/linuxdeploy-root/plugins/linuxdeploy-plugin-appimage/usr/bin/appimagetool" "$appdir"
generated_appimage="$(find "$PWD" -maxdepth 1 -type f -name '*.AppImage' ! -name 'linuxdeploy*.AppImage' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print $2}')"
if [ -z "$generated_appimage" ]; then
echo "::error::Generated AppImage not found for $exec_name"
exit 1
fi
mv "$generated_appimage" "$PWD/release/$output_name"
}
build_cli_appimage "$PWD/src/.libs/verged" "Verge Daemon" "verged" "verged-aarch64.AppImage" "false"
build_cli_appimage "$PWD/src/.libs/verge-cli" "Verge CLI" "verge-cli" "verge-cli-aarch64.AppImage" "true"
build_cli_appimage "$PWD/src/.libs/verge-tx" "Verge TX" "verge-tx" "verge-tx-aarch64.AppImage" "true"
(
cd "$PWD/release"
zip -j verge-aarch64-pi5-appimages.zip \
Verge-Qt-aarch64.AppImage \
verged-aarch64.AppImage \
verge-cli-aarch64.AppImage \
verge-tx-aarch64.AppImage
)
- uses: actions/upload-artifact@v4
with:
name: verge-aarch64-pi5
path: |
./release/Verge-Qt-aarch64.AppImage
./release/verged-aarch64.AppImage
./release/verge-cli-aarch64.AppImage
./release/verge-tx-aarch64.AppImage
./release/verge-aarch64-pi5-appimages.zip
windows64:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
base-devel
git
make
autoconf
automake
libtool
pkgconf
gperf
flex
bison
diffutils
patch
curl
file
unzip
zip
tar
perl
python
mingw-w64-x86_64-nsis
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Set up MSVC x64 environment
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Cache MSVC bootstrap artifacts
uses: actions/cache@v4
with:
path: |
build-msvc
key: windows64-msvc-bootstrap-v1-${{ hashFiles('contrib/windows-msvc/**') }}
restore-keys: |
windows64-msvc-bootstrap-v1-
- name: Bootstrap native Windows MSVC dependencies
shell: pwsh
run: |
./contrib/windows-msvc/bootstrap.ps1
. ./build-msvc/env-msvc.ps1
@(
"QTDIR=$env:QTDIR"
"Qt6_DIR=$env:Qt6_DIR"
"BOOST_ROOT=$env:BOOST_ROOT"
"BOOST_INCLUDEDIR=$env:BOOST_INCLUDEDIR"
"BOOST_LIBRARYDIR=$env:BOOST_LIBRARYDIR"
"OPENSSL_ROOT_DIR=$env:OPENSSL_ROOT_DIR"
"OPENSSL_INCLUDE_DIR=$env:OPENSSL_INCLUDE_DIR"
"OPENSSL_LIB_DIR=$env:OPENSSL_LIB_DIR"
"BDB_PREFIX=$env:BDB_PREFIX"
"BDB_CFLAGS=$env:BDB_CFLAGS"
"BDB_LIBS=$env:BDB_LIBS"
"VCPKG_ROOT=$env:VCPKG_ROOT"
"VCPKG_INSTALLED_DIR=$env:VCPKG_INSTALLED_DIR"
"VCPKG_DEFAULT_TRIPLET=$env:VCPKG_DEFAULT_TRIPLET"
"VERGE_MSVC_PKGCONFIG=$env:VERGE_MSVC_PKGCONFIG"
"PKG_CONFIG_PATH=$env:PKG_CONFIG_PATH"
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Build native Windows64 with MSVC dependencies
run: |
set -euo pipefail
BUNDLE="$PWD/release/windows64"
bash ./contrib/windows-msvc/build-msys.sh
mkdir -p "$BUNDLE"
cp "$PWD/src/verged.exe" "$BUNDLE/" || true
cp "$PWD/src/verge-cli.exe" "$BUNDLE/" || true
cp "$PWD/src/verge-tx.exe" "$BUNDLE/" || true
cp "$PWD/src/qt/verge-qt.exe" "$BUNDLE/" || true
- name: Bundle Qt runtime
shell: pwsh
run: |
. ./build-msvc/env-msvc.ps1
$bundle = Join-Path $PWD "release/windows64"
$qtExe = Join-Path $bundle "verge-qt.exe"
if (Test-Path $qtExe) {
& (Join-Path $env:QTDIR "bin/windeployqt.exe") --release --compiler-runtime --dir $bundle $qtExe
}
$bdbDll = Get-ChildItem (Join-Path $PWD "build-msvc/deps/bdb") -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^libdb48\.dll$|^db48\.dll$|^libdb.*48.*\.dll$|^db.*48.*\.dll$' } |
Select-Object -First 1
if ($bdbDll) {
Copy-Item $bdbDll.FullName (Join-Path $bundle $bdbDll.Name) -Force
}
- name: Build Windows installer
shell: pwsh
run: |
$template = Join-Path $PWD "contrib/windows-msvc/verge-installer.nsi.in"
$script = Join-Path $PWD "build-msvc/verge-installer.nsi"
$bundle = (Resolve-Path (Join-Path $PWD "release/windows64")).Path
$output = Join-Path $PWD "release/verge-windows64-setup.exe"
$icon = (Resolve-Path (Join-Path $PWD "share/pixmaps/verge.ico")).Path
$wizardSource = (Resolve-Path (Join-Path $PWD "share/pixmaps/nsis-wizard.png")).Path
$wizard = Join-Path $PWD "build-msvc/verge-installer.bmp"
$header = (Resolve-Path (Join-Path $PWD "share/pixmaps/nsis-header.bmp")).Path
$license = (Resolve-Path (Join-Path $PWD "COPYING")).Path
Add-Type -AssemblyName System.Drawing
$wizardImage = [System.Drawing.Image]::FromFile($wizardSource)
$wizardBitmap = New-Object System.Drawing.Bitmap $wizardImage
try {
$wizardBitmap.Save($wizard, [System.Drawing.Imaging.ImageFormat]::Bmp)
} finally {
$wizardBitmap.Dispose()
$wizardImage.Dispose()
}
$scriptBody = Get-Content $template -Raw
$replacements = @{
'@APP_NAME@' = 'Verge (64-bit)'
'@OUTPUT_EXE@' = $output
'@BUNDLE_DIR@' = $bundle
'@ICON_FILE@' = $icon
'@WIZARD_BITMAP@' = $wizard
'@HEADER_BITMAP@' = $header
'@LICENSE_FILE@' = $license
}
foreach ($entry in $replacements.GetEnumerator()) {
$scriptBody = $scriptBody.Replace($entry.Key, $entry.Value)
}
Set-Content -Path $script -Value $scriptBody -Encoding ASCII -NoNewline
$makensisCandidates = @(
(Join-Path $env:RUNNER_TEMP "msys64/mingw64/bin/makensis.exe"),
(Join-Path $env:RUNNER_TEMP "msys64/usr/bin/makensis.exe")
)
$makensis = $makensisCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $makensis) {
throw "makensis.exe not found under $env:RUNNER_TEMP\\msys64"
}
& $makensis -V2 $script
- name: Dump config.log on failure
if: failure()
run: |
test -f config.log && sed -n '1,200p' config.log || true
test -f config.status && sed -n '1,200p' config.status || true
test -f src/tor/config.log && sed -n '1,260p' src/tor/config.log || true
test -f src/tor/config.log && grep -nE -C 20 'evdns_base_new|event_new|libevent2 is installed but linking it failed|undefined reference|undefined symbol|lld-link|linker command failed' src/tor/config.log || true
test -f src/tor/config.status && sed -n '1,200p' src/tor/config.status || true
find build-msvc/vcpkg/installed -maxdepth 4 -type f \( -iname 'event*.lib' -o -iname 'libevent*.lib' -o -iname 'libevent*.pc' \) -print || true
- uses: actions/upload-artifact@v4
with:
name: verge-windows64bit
path: |
release/windows64
release/verge-windows64-setup.exe