Merge pull request #177 from mappu/miqt-android-qt6

Android Qt 6 support
This commit is contained in:
mappu 2025-02-15 18:40:30 +13:00 committed by GitHub
commit f0517fb088
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 264 additions and 34 deletions

View File

@ -40,7 +40,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: linux64-gocache key: linux64-qt5-gocache
- name: Linux64 bindings compile and test - name: Linux64 bindings compile and test
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/linux64:qt5 /bin/bash -c 'cd qt && go build && cd ../examples/marshalling && env QT_QPA_PLATFORM=offscreen go test -v' run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/linux64:qt5 /bin/bash -c 'cd qt && go build && cd ../examples/marshalling && env QT_QPA_PLATFORM=offscreen go test -v'
@ -94,7 +94,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: win32-gocache key: win32-qt5-gocache
- name: Win32 docker build - name: Win32 docker build
run: docker build -t miqt/win32:qt5 -f docker/win32-cross-go1.23-qt5.15-static.Dockerfile . run: docker build -t miqt/win32:qt5 -f docker/win32-cross-go1.23-qt5.15-static.Dockerfile .
@ -113,7 +113,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: win64-gocache key: win64-qt5-gocache
- name: Win64 docker build - name: Win64 docker build
run: docker build -t miqt/win64:qt5 -f docker/win64-cross-go1.23-qt5.15-static.Dockerfile . run: docker build -t miqt/win64:qt5 -f docker/win64-cross-go1.23-qt5.15-static.Dockerfile .
@ -132,7 +132,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: win64-gocache key: win64-qt68-gocache
- name: Win64 docker build - name: Win64 docker build
run: docker build -t miqt/win64:qt68 -f docker/win64-cross-go1.23-qt6.8-dynamic.Dockerfile . run: docker build -t miqt/win64:qt68 -f docker/win64-cross-go1.23-qt6.8-dynamic.Dockerfile .
@ -154,7 +154,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: android-armv8a-gocache key: android-qt5-armv8a-gocache
- name: Android compile app as c-shared my_go_app.so - name: Android compile app as c-shared my_go_app.so
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src/examples/android miqt/android:qt5 go build -buildmode c-shared -ldflags "-s -w -extldflags -Wl,-soname,my_go_app.so" -o android-build/libs/arm64-v8a/my_go_app.so run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src/examples/android miqt/android:qt5 go build -buildmode c-shared -ldflags "-s -w -extldflags -Wl,-soname,my_go_app.so" -o android-build/libs/arm64-v8a/my_go_app.so
@ -170,3 +170,34 @@ jobs:
- name: Verify that package exists - name: Verify that package exists
run: test -f examples/android/android-build/build/outputs/apk/debug/android-build-debug.apk run: test -f examples/android/android-build/build/outputs/apk/debug/android-build-debug.apk
miqt_android_qt6:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Android armv8a docker build
run: docker build -t miqt/android:qt6 -f docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile .
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: android-qt6-armv8a-gocache
- name: Android compile app as c-shared my_go_app.so
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src/examples/android6 miqt/android:qt6 go build -buildmode c-shared -ldflags "-s -w -extldflags -Wl,-soname,my_go_app.so" -o android-build/libs/arm64-v8a/my_go_app.so
- name: Android generate libRealAppName.so linking stub
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src/examples/android6 miqt/android:qt6 android-stub-gen.sh my_go_app.so AndroidMain android-build/libs/arm64-v8a/libRealAppName_arm64-v8a.so --qt6
- name: Android generate json packaging metadata
run: docker run --rm -v $(pwd):/src -w /src/examples/android6 miqt/android:qt6 android-mktemplate.sh RealAppName deployment-settings.json
- name: Android build APK package
run: docker run --rm -v $(pwd):/src -w /src/examples/android6 miqt/android:qt6 androiddeployqt --input ./deployment-settings.json --output ./android-build/
- name: Verify that package exists
run: test -f examples/android6/android-build/build/outputs/apk/debug/android-build-debug.apk

View File

@ -275,6 +275,8 @@ See FAQ Q3 for advice about docker performance.
*Tested with Raymii Qt 5.15 / Android SDK 31 / Android NDK 22* *Tested with Raymii Qt 5.15 / Android SDK 31 / Android NDK 22*
*Tested with Qt.io Qt 6.6 / Android SDK 33 / Android NDK 25*
MIQT supports compiling for Android. Some extra steps are required to bridge the Java, C++, Go worlds. MIQT supports compiling for Android. Some extra steps are required to bridge the Java, C++, Go worlds.
![](doc/android-architecture.png) ![](doc/android-architecture.png)
@ -285,11 +287,13 @@ MIQT supports compiling for Android. Some extra steps are required to bridge the
- Ensure to `import "C"`. - Ensure to `import "C"`.
- Check `examples/android` to see how to support both Android and desktop platforms. - Check `examples/android` to see how to support both Android and desktop platforms.
2. Build the necessary docker container for cross-compilation: 2. Build the necessary docker container for cross-compilation:
- `docker build -t miqt/android:latest -f docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile .` - (Qt 5) `docker build -t miqt/android:latest -f docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile .`
- (Qt 6) `docker build -t miqt/android:latest -f docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile .`
3. Build your application as `.so` format: 3. Build your application as `.so` format:
- `docker run --rm -v $(pwd):/src -w /src miqt/android:latest go build -buildmode c-shared -ldflags "-s -w -extldflags -Wl,-soname,my_go_app.so" -o android-build/libs/arm64-v8a/my_go_app.so` - `docker run --rm -v $(pwd):/src -w /src miqt/android:latest go build -buildmode c-shared -ldflags "-s -w -extldflags -Wl,-soname,my_go_app.so" -o android-build/libs/arm64-v8a/my_go_app.so`
4. Build the Qt linking stub: 4. Build the Qt linking stub:
- `docker run --rm -v $(pwd):/src -w /src miqt/android:latest android-stub-gen.sh my_go_app.so AndroidMain android-build/libs/arm64-v8a/libRealAppName_arm64-v8a.so` - (Qt 5) `docker run --rm -v $(pwd):/src -w /src miqt/android:latest android-stub-gen.sh my_go_app.so AndroidMain android-build/libs/arm64-v8a/libRealAppName_arm64-v8a.so`
- (Qt 6) Add `--qt6` final argument
- The linking stub is needed because Qt for Android will itself only call a function named `main`, but `c-shared` can't create one. - The linking stub is needed because Qt for Android will itself only call a function named `main`, but `c-shared` can't create one.
5. Build the [androiddeployqt](https://doc.qt.io/qt-6/android-deploy-qt-tool.html) configuration file: 5. Build the [androiddeployqt](https://doc.qt.io/qt-6/android-deploy-qt-tool.html) configuration file:
- `docker run --rm -v $(pwd):/src -w /src miqt/android:latest android-mktemplate.sh RealAppName deployment-settings.json` - `docker run --rm -v $(pwd):/src -w /src miqt/android:latest android-mktemplate.sh RealAppName deployment-settings.json`

View File

@ -7,6 +7,27 @@ set -eu
# QT_PATH is already pre-set in our docker container environment. Includes trailing slash. # QT_PATH is already pre-set in our docker container environment. Includes trailing slash.
QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/} QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/}
QT_ANDROID=${QT_ANDROID:-$QT_PATH}
ndk_version() {
ls /opt/android-sdk/ndk/ | tail -n1
}
target_sdk_version() {
ls /opt/android-sdk/platforms | tail -n1 | sed -re 's/android-//'
}
build_tools_version() {
ls /opt/android-sdk/build-tools | tail -n1
}
extra_libs() {
if [[ -d /opt/android_openssl ]] ; then
# Our miqt Qt5 container includes these extra .so libraries
# However, the aqtinstall-based Qt 6 container does not use them
echo "/opt/android_openssl/ssl_1.1/arm64-v8a/libssl_1_1.so,/opt/android_openssl/ssl_1.1/arm64-v8a/libcrypto_1_1.so"
fi
}
main() { main() {
@ -26,17 +47,15 @@ main() {
"architectures": { "architectures": {
"arm64-v8a" : "aarch64-linux-android" "arm64-v8a" : "aarch64-linux-android"
}, },
"android-extra-libs": "$(extra_libs)",
"android-extra-libs": "/opt/android_openssl/ssl_1.1/arm64-v8a/libssl_1_1.so,/opt/android_openssl/ssl_1.1/arm64-v8a/libcrypto_1_1.so",
"android-min-sdk-version": "23", "android-min-sdk-version": "23",
"android-target-sdk-version": "30", "android-target-sdk-version": "$(target_sdk_version)",
"ndk": "/opt/android-sdk/ndk/22.1.7171670", "ndk": "/opt/android-sdk/ndk/$(ndk_version)",
"ndk-host": "linux-x86_64", "ndk-host": "linux-x86_64",
"qt": "${QT_PATH}", "qt": "${QT_ANDROID}",
"sdk": "/opt/android-sdk", "sdk": "/opt/android-sdk",
"sdkBuildToolsRevision": "30.0.2", "sdkBuildToolsRevision": "$(build_tools_version)",
"stdcpp-path": "/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/", "stdcpp-path": "/opt/android-sdk/ndk/$(ndk_version)/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/",
"tool-prefix": "llvm", "tool-prefix": "llvm",
"toolchain-prefix": "llvm", "toolchain-prefix": "llvm",
"useLLVM": true "useLLVM": true

View File

@ -6,24 +6,21 @@
set -eu set -eu
# QT_PATH is already pre-set in our docker container environment. Includes trailing slash.
QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/}
main() { main() {
if [[ $# -ne 3 ]] ; then if [[ $# -ne 3 && $# -ne 4 ]] ; then
echo "Usage: android-gen-stub.sh src.so function-name dest.so" >&2 echo "Usage: android-gen-stub.sh src.so function-name dest.so [--qt6|--qt5]" >&2
exit 1 exit 1
fi fi
local ARG_SOURCE_SOFILE="$1" local ARG_SOURCE_SOFILE="$1"
local ARG_FUNCTIONNAME="$2" local ARG_FUNCTIONNAME="$2"
local ARG_DEST_SOFILE="$3" local ARG_DEST_SOFILE="$3"
local ARG_QTVERSION="${4:---qt5}"
local tmpdir=$(mktemp -d) local tmpdir=$(mktemp -d)
trap "rm -r ${tmpdir}" EXIT trap "rm -r ${tmpdir}" EXIT
echo "- Using temporary directory: ${tmpdir}" echo "- Using temporary directory: ${tmpdir}"
echo "- Found Qt path: ${QT_PATH}"
echo "Generating stub..." echo "Generating stub..."
@ -64,19 +61,44 @@ EOF
# Compile # Compile
# Link with Qt libraries so that androiddeployqt detects us as being the # Link with Qt libraries so that androiddeployqt detects us as being the
# main shared library # main shared library
$CXX -shared \
-ldl \ if [[ $ARG_QTVERSION == '--qt5' ]] ; then
-llog \
${QT_PATH}plugins/platforms/libplugins_platforms_qtforandroid_arm64-v8a.so \ # QT_PATH is already pre-set in our docker container environment. Includes trailing slash.
${QT_PATH}lib/libQt5Widgets_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5Gui_arm64-v8a.so \ QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/}
${QT_PATH}lib/libQt5Core_arm64-v8a.so \ echo "- Found Qt path: ${QT_PATH}"
${QT_PATH}lib/libQt5Svg_arm64-v8a.so \
${QT_PATH}lib/libQt5AndroidExtras_arm64-v8a.so \
-fPIC -DQT_WIDGETS_LIB -I${QT_PATH}include/QtWidgets -I${QT_PATH}include/ -I${QT_PATH}include/QtCore -DQT_GUI_LIB -I${QT_PATH}include/QtGui -DQT_CORE_LIB \
$tmpdir/miqtstub.cpp \
"-Wl,-soname,$(basename "$ARG_DEST_SOFILE")" \
-o "$ARG_DEST_SOFILE"
$CXX -shared \
-ldl \
-llog \
-L${QT_PATH}plugins/platforms -lplugins_platforms_qtforandroid_arm64-v8a \
$(pkg-config --libs Qt5Widgets) \
$(pkg-config --libs Qt5AndroidExtras) \
$tmpdir/miqtstub.cpp \
"-Wl,-soname,$(basename "$ARG_DEST_SOFILE")" \
-o "$ARG_DEST_SOFILE"
elif [[ $ARG_QTVERSION == '--qt6' ]] ; then
# QT_ANDROID is already pre-set in our docker container environment. Does NOT include trailing slash
QT_ANDROID=${QT_ANDROID:-/opt/Qt/6.6.1/android_arm64_v8a}
echo "- Found Qt path: ${QT_ANDROID}"
# There is no AndroidExtras in Qt 6
$CXX -shared \
-ldl \
-llog \
-L${QT_ANDROID}/plugins/platforms -lplugins_platforms_qtforandroid_arm64-v8a \
$(pkg-config --libs Qt6Widgets) \
$tmpdir/miqtstub.cpp \
"-Wl,-soname,$(basename "$ARG_DEST_SOFILE")" \
-o "$ARG_DEST_SOFILE"
else
echo "Unknown Qt version argument "${ARG_QTVERSION}" (expected --qt5 or --qt6)" >&2
exit 1
fi
echo "Done." echo "Done."
} }

View File

@ -105,6 +105,35 @@ func Widgets_AllowHeader(fullpath string) bool {
return true return true
} }
type AllowedPlatformInfo interface {
GoBuildTag() string
CxxIf() string
}
type AndroidBlockedPlatform struct{}
func (abp AndroidBlockedPlatform) GoBuildTag() string {
return `!android`
}
func (abp AndroidBlockedPlatform) CxxIf() string {
return `! defined(Q_OS_ANDROID)`
}
func HeaderPlatformRestriction(fullpath string) AllowedPlatformInfo {
fname := filepath.Base(fullpath)
if fname == `qsharedmemory.h` {
// Not implemented on Android nor iOS
// Qt 5: Classes are present but do not work
// Qt 6: Class definition is not present and our generated subclass fails to compile
return AndroidBlockedPlatform{}
}
// No platform restriction
return nil
}
func ImportHeaderForClass(className string) bool { func ImportHeaderForClass(className string) bool {
if className[0] != 'Q' { if className[0] != 'Q' {
return false return false

View File

@ -931,6 +931,16 @@ func emitBindingCpp(src *CppParsedHeader, filename string) (string, error) {
ret.WriteString(`#include <` + filename + ">\n") ret.WriteString(`#include <` + filename + ">\n")
ret.WriteString(`#include "gen_` + filename + "\"\n") ret.WriteString(`#include "gen_` + filename + "\"\n")
// Perform any platform checks
// n.b. The Q_OS_ variable is defined usually indirectly from another Qt
// header, so it should be checked only after all the other includes,
// although that seems suboptimal
platformRestriction := HeaderPlatformRestriction(filename)
if platformRestriction != nil {
ret.WriteString(`#if ` + platformRestriction.CxxIf() + "\n\n")
}
// Write prototypes for functions that the host language bindings should export // Write prototypes for functions that the host language bindings should export
// for virtual function overrides // for virtual function overrides
@ -1410,5 +1420,11 @@ extern "C" {
} }
} }
//
if platformRestriction != nil {
ret.WriteString(`#endif //` + platformRestriction.CxxIf() + "\n\n")
}
return ret.String(), nil return ret.String(), nil
} }

View File

@ -669,6 +669,13 @@ func (gfs *goFileState) emitCabiToGo(assignExpr string, rt CppParameter, rvalue
func emitGo(src *CppParsedHeader, headerName string, packageName string) (string, string, error) { func emitGo(src *CppParsedHeader, headerName string, packageName string) (string, string, error) {
ret := strings.Builder{} ret := strings.Builder{}
platformRestriction := HeaderPlatformRestriction(headerName)
if platformRestriction != nil {
ret.WriteString(`//go:build ` + platformRestriction.GoBuildTag() + "\n" +
`// +build ` + platformRestriction.GoBuildTag() + "\n\n")
}
ret.WriteString(`package ` + path.Base(packageName) + ` ret.WriteString(`package ` + path.Base(packageName) + `
/* /*

View File

@ -21,7 +21,7 @@ ENV GOOS=android
ENV GOARCH=arm64 ENV GOARCH=arm64
ENV GOFLAGS=-buildvcs=false ENV GOFLAGS=-buildvcs=false
ENV PKG_CONFIG_PATH=/usr/local/Qt-5.15.13/lib/pkgconfig ENV PKG_CONFIG_PATH=/usr/local/Qt-5.15.13/lib/pkgconfig
ENV CGO_CXXFLAGS="-Wno-ignored-attributes -D_Bool=bool" ENV CGO_CXXFLAGS="-Wno-ignored-attributes"
# Reset the ENTRYPOINT # Reset the ENTRYPOINT
ENTRYPOINT [] ENTRYPOINT []

View File

@ -0,0 +1,38 @@
FROM stateoftheartio/qt6:6.6-android-aqt
# The base image sets us to uid:gid 1000:999. Revert it so we can run apt
USER root
RUN apt update && apt install -qyy wget pkg-config && apt autoclean
RUN wget 'https://go.dev/dl/go1.23.6.linux-amd64.tar.gz' && \
tar x -C /usr/local/ -f go1.23.6.linux-amd64.tar.gz && \
rm go1.23.6.linux-amd64.tar.gz
COPY cmd/android-stub-gen/android-stub-gen.sh /usr/local/bin/android-stub-gen.sh
COPY cmd/android-mktemplate/android-mktemplate.sh /usr/local/bin/android-mktemplate.sh
# Fix up pkg-config definitions:
# 1. There are only pkg-config definitions included for gcc_64 (Linux native), not for the android_arm64_v8a target we actually want
# 2. It looks for `Libs: -L${libdir} -lQt6Widgets` but the file is named libQt6Widgets_arm64-v8a.so
RUN mkdir -p /opt/Qt/6.6.1/android_arm64_v8a/lib/pkgconfig/ && \
cp /opt/Qt/6.6.1/gcc_64/lib/pkgconfig/*.pc /opt/Qt/6.6.1/android_arm64_v8a/lib/pkgconfig/ && \
find /opt/Qt/6.6.1/android_arm64_v8a/lib/pkgconfig/ -type f -exec sed -i -re 's~gcc_64~android_arm64_v8a~' {} \; && \
find /opt/Qt/6.6.1/android_arm64_v8a/lib/pkgconfig/ -type f -exec sed -i -re 's~-l(Q[^ ]+)~-l\1_arm64-v8a~' {} \;
# The final step of building a Miqt app for android is running androiddeployqt
# This binary only exists in the gcc_64 target which is not in $PATH
# Add a symlink
RUN ln -s /opt/Qt/6.6.1/gcc_64/bin/androiddeployqt /usr/local/bin/androiddeployqt
ENV PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/Qt/Tools/CMake/bin:/opt/Qt/Tools/Ninja:/opt/Qt/6.6.1/android_arm64_v8a/bin:/opt/android-sdk/cmdline-tools/10.0/bin:/opt/android-sdk/platform-tools
ENV CC=/opt/android-sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang
ENV CXX=/opt/android-sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang++
ENV CGO_ENABLED=1
ENV GOOS=android
ENV GOARCH=arm64
ENV GOFLAGS=-buildvcs=false
ENV PKG_CONFIG_PATH=/opt/Qt/6.6.1/android_arm64_v8a/lib/pkgconfig
ENV CGO_CXXFLAGS="--std=c++17 -Wno-ignored-attributes"

29
examples/android6/main.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"fmt"
"os"
qt "github.com/mappu/miqt/qt6"
)
func myRealMainFunc() {
qt.NewQApplication(os.Args)
btn := qt.NewQPushButton3("Hello world!")
btn.SetFixedWidth(320)
var counter int = 0
btn.OnPressed(func() {
counter++
btn.SetText(fmt.Sprintf("You have clicked the button %d time(s)", counter))
})
btn.Show()
qt.QApplication_Exec()
fmt.Println("OK!")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,14 @@
// +build android
package main
import "C" // Required for export support
//export AndroidMain
func AndroidMain() {
myRealMainFunc()
}
func main() {
// Must be empty
}

View File

@ -0,0 +1,7 @@
// +build !android
package main
func main() {
myRealMainFunc()
}

View File

@ -10,6 +10,8 @@
#include <QTimerEvent> #include <QTimerEvent>
#include <qsharedmemory.h> #include <qsharedmemory.h>
#include "gen_qsharedmemory.h" #include "gen_qsharedmemory.h"
#if ! defined(Q_OS_ANDROID)
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -539,3 +541,5 @@ void QSharedMemory_delete(QSharedMemory* self) {
delete self; delete self;
} }
#endif //! defined(Q_OS_ANDROID)

View File

@ -1,3 +1,6 @@
//go:build !android
// +build !android
package qt package qt
/* /*

View File

@ -10,6 +10,8 @@
#include <QTimerEvent> #include <QTimerEvent>
#include <qsharedmemory.h> #include <qsharedmemory.h>
#include "gen_qsharedmemory.h" #include "gen_qsharedmemory.h"
#if ! defined(Q_OS_ANDROID)
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -507,3 +509,5 @@ void QSharedMemory_delete(QSharedMemory* self) {
delete self; delete self;
} }
#endif //! defined(Q_OS_ANDROID)

View File

@ -1,3 +1,6 @@
//go:build !android
// +build !android
package qt6 package qt6
/* /*