From 1a29763a4b99aef96b14442b892f93764a39f9fc Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:09:04 +1300 Subject: [PATCH 01/10] docker: qt5-android: the -D_Bool=bool trick is no longer required --- docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile index 69fd887e..69424071 100644 --- a/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile +++ b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile @@ -21,7 +21,7 @@ ENV GOOS=android ENV GOARCH=arm64 ENV GOFLAGS=-buildvcs=false 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 ENTRYPOINT [] From f8e429b362f86172e4b9b4f927a81cf6df62e4e5 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:09:42 +1300 Subject: [PATCH 02/10] docker: qt6-android: add initial container based on aqtinstall --- ...oid-armv8a-go1.23-qt6.6-dynamic.Dockerfile | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile diff --git a/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile b/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile new file mode 100644 index 00000000..deb2f147 --- /dev/null +++ b/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile @@ -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" From da874c95ed1220c1b60ba71c0a9ba562d7693325 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:10:03 +1300 Subject: [PATCH 03/10] genbindings: prevent using qt6 qsharedmemory on android --- cmd/genbindings/config-allowlist.go | 29 +++++++++++++++++++++++++++++ cmd/genbindings/emitcabi.go | 16 ++++++++++++++++ cmd/genbindings/emitgo.go | 7 +++++++ 3 files changed, 52 insertions(+) diff --git a/cmd/genbindings/config-allowlist.go b/cmd/genbindings/config-allowlist.go index 91ae0072..60fce8f7 100644 --- a/cmd/genbindings/config-allowlist.go +++ b/cmd/genbindings/config-allowlist.go @@ -105,6 +105,35 @@ func Widgets_AllowHeader(fullpath string) bool { 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 { if className[0] != 'Q' { return false diff --git a/cmd/genbindings/emitcabi.go b/cmd/genbindings/emitcabi.go index 7014f7e8..1cabff09 100644 --- a/cmd/genbindings/emitcabi.go +++ b/cmd/genbindings/emitcabi.go @@ -931,6 +931,16 @@ func emitBindingCpp(src *CppParsedHeader, filename string) (string, error) { ret.WriteString(`#include <` + 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 // for virtual function overrides @@ -1410,5 +1420,11 @@ extern "C" { } } + // + + if platformRestriction != nil { + ret.WriteString(`#endif //` + platformRestriction.CxxIf() + "\n\n") + } + return ret.String(), nil } diff --git a/cmd/genbindings/emitgo.go b/cmd/genbindings/emitgo.go index 53a16dab..57866b35 100644 --- a/cmd/genbindings/emitgo.go +++ b/cmd/genbindings/emitgo.go @@ -669,6 +669,13 @@ func (gfs *goFileState) emitCabiToGo(assignExpr string, rt CppParameter, rvalue func emitGo(src *CppParsedHeader, headerName string, packageName string) (string, string, error) { 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) + ` /* From 53ae4b7d1652733900546547c6b3865cf5fb3f94 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:10:12 +1300 Subject: [PATCH 04/10] qt: rebuild (prevent using qsharedmemory on android) --- qt/gen_qsharedmemory.cpp | 4 ++++ qt/gen_qsharedmemory.go | 3 +++ qt6/gen_qsharedmemory.cpp | 4 ++++ qt6/gen_qsharedmemory.go | 3 +++ 4 files changed, 14 insertions(+) diff --git a/qt/gen_qsharedmemory.cpp b/qt/gen_qsharedmemory.cpp index 0167c888..8d946666 100644 --- a/qt/gen_qsharedmemory.cpp +++ b/qt/gen_qsharedmemory.cpp @@ -10,6 +10,8 @@ #include #include #include "gen_qsharedmemory.h" +#if ! defined(Q_OS_ANDROID) + #ifdef __cplusplus extern "C" { @@ -539,3 +541,5 @@ void QSharedMemory_delete(QSharedMemory* self) { delete self; } +#endif //! defined(Q_OS_ANDROID) + diff --git a/qt/gen_qsharedmemory.go b/qt/gen_qsharedmemory.go index 5f576202..b45a1896 100644 --- a/qt/gen_qsharedmemory.go +++ b/qt/gen_qsharedmemory.go @@ -1,3 +1,6 @@ +//go:build !android +// +build !android + package qt /* diff --git a/qt6/gen_qsharedmemory.cpp b/qt6/gen_qsharedmemory.cpp index 1623d8a6..99881172 100644 --- a/qt6/gen_qsharedmemory.cpp +++ b/qt6/gen_qsharedmemory.cpp @@ -10,6 +10,8 @@ #include #include #include "gen_qsharedmemory.h" +#if ! defined(Q_OS_ANDROID) + #ifdef __cplusplus extern "C" { @@ -507,3 +509,5 @@ void QSharedMemory_delete(QSharedMemory* self) { delete self; } +#endif //! defined(Q_OS_ANDROID) + diff --git a/qt6/gen_qsharedmemory.go b/qt6/gen_qsharedmemory.go index 18acb2c1..a090e3c3 100644 --- a/qt6/gen_qsharedmemory.go +++ b/qt6/gen_qsharedmemory.go @@ -1,3 +1,6 @@ +//go:build !android +// +build !android + package qt6 /* From 98567bf4eed068a4bb4f8338a8ec235f0fb07a79 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:10:26 +1300 Subject: [PATCH 05/10] android: update mktemplate and stub-gen for the new qt6-android container --- cmd/android-mktemplate/android-mktemplate.sh | 35 +++++++++--- cmd/android-stub-gen/android-stub-gen.sh | 58 ++++++++++++++------ 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/cmd/android-mktemplate/android-mktemplate.sh b/cmd/android-mktemplate/android-mktemplate.sh index d42e2587..8ccc3f0a 100755 --- a/cmd/android-mktemplate/android-mktemplate.sh +++ b/cmd/android-mktemplate/android-mktemplate.sh @@ -7,6 +7,27 @@ 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/} +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() { @@ -26,17 +47,15 @@ main() { "architectures": { "arm64-v8a" : "aarch64-linux-android" }, - - "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-extra-libs": "$(extra_libs)", "android-min-sdk-version": "23", - "android-target-sdk-version": "30", - "ndk": "/opt/android-sdk/ndk/22.1.7171670", + "android-target-sdk-version": "$(target_sdk_version)", + "ndk": "/opt/android-sdk/ndk/$(ndk_version)", "ndk-host": "linux-x86_64", - "qt": "${QT_PATH}", + "qt": "${QT_ANDROID}", "sdk": "/opt/android-sdk", - "sdkBuildToolsRevision": "30.0.2", - "stdcpp-path": "/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/", + "sdkBuildToolsRevision": "$(build_tools_version)", + "stdcpp-path": "/opt/android-sdk/ndk/$(ndk_version)/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/", "tool-prefix": "llvm", "toolchain-prefix": "llvm", "useLLVM": true diff --git a/cmd/android-stub-gen/android-stub-gen.sh b/cmd/android-stub-gen/android-stub-gen.sh index 2f1b75dd..ac5b5b26 100755 --- a/cmd/android-stub-gen/android-stub-gen.sh +++ b/cmd/android-stub-gen/android-stub-gen.sh @@ -6,24 +6,21 @@ 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() { - if [[ $# -ne 3 ]] ; then - echo "Usage: android-gen-stub.sh src.so function-name dest.so" >&2 + if [[ $# -ne 3 && $# -ne 4 ]] ; then + echo "Usage: android-gen-stub.sh src.so function-name dest.so [--qt6|--qt5]" >&2 exit 1 fi local ARG_SOURCE_SOFILE="$1" local ARG_FUNCTIONNAME="$2" local ARG_DEST_SOFILE="$3" + local ARG_QTVERSION="${4:---qt5}" local tmpdir=$(mktemp -d) trap "rm -r ${tmpdir}" EXIT echo "- Using temporary directory: ${tmpdir}" - echo "- Found Qt path: ${QT_PATH}" echo "Generating stub..." @@ -64,19 +61,44 @@ EOF # Compile # Link with Qt libraries so that androiddeployqt detects us as being the # main shared library - $CXX -shared \ - -ldl \ - -llog \ - ${QT_PATH}plugins/platforms/libplugins_platforms_qtforandroid_arm64-v8a.so \ - ${QT_PATH}lib/libQt5Widgets_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5Gui_arm64-v8a.so \ - ${QT_PATH}lib/libQt5Core_arm64-v8a.so \ - ${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" + + if [[ $ARG_QTVERSION == '--qt5' ]] ; then + + # QT_PATH is already pre-set in our docker container environment. Includes trailing slash. + QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/} + echo "- Found Qt path: ${QT_PATH}" + $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." } From a67d7cfe65f6bdb99fd9f50641492dc1d2543801 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:10:34 +1300 Subject: [PATCH 06/10] examples: add android6 example --- examples/android6/main.go | 29 ++++++++++++++++++++++++++++ examples/android6/startup_android.go | 14 ++++++++++++++ examples/android6/startup_other.go | 7 +++++++ 3 files changed, 50 insertions(+) create mode 100644 examples/android6/main.go create mode 100644 examples/android6/startup_android.go create mode 100644 examples/android6/startup_other.go diff --git a/examples/android6/main.go b/examples/android6/main.go new file mode 100644 index 00000000..7ce9703b --- /dev/null +++ b/examples/android6/main.go @@ -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!") +} diff --git a/examples/android6/startup_android.go b/examples/android6/startup_android.go new file mode 100644 index 00000000..2fbc4d5b --- /dev/null +++ b/examples/android6/startup_android.go @@ -0,0 +1,14 @@ +// +build android + +package main + +import "C" // Required for export support + +//export AndroidMain +func AndroidMain() { + myRealMainFunc() +} + +func main() { + // Must be empty +} diff --git a/examples/android6/startup_other.go b/examples/android6/startup_other.go new file mode 100644 index 00000000..d2382476 --- /dev/null +++ b/examples/android6/startup_other.go @@ -0,0 +1,7 @@ +// +build !android + +package main + +func main() { + myRealMainFunc() +} From 6eb36509185351237dde91448484af6aa9ee6834 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 18:21:01 +1300 Subject: [PATCH 07/10] examples/android6: add screenshot from built apk --- examples/android6/screenshot.png | Bin 0 -> 27865 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/android6/screenshot.png diff --git a/examples/android6/screenshot.png b/examples/android6/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f5def8a6e8610fe5e21c4f425d598c93c3b24a7c GIT binary patch literal 27865 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 Date: Sat, 15 Feb 2025 17:13:16 +1300 Subject: [PATCH 08/10] github/ci: include version/platform in all cache key names --- .github/workflows/miqt.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/miqt.yml b/.github/workflows/miqt.yml index 00088cc3..9d90ff34 100644 --- a/.github/workflows/miqt.yml +++ b/.github/workflows/miqt.yml @@ -40,7 +40,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/go-build - key: linux64-gocache + key: linux64-qt5-gocache - 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' @@ -94,7 +94,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/go-build - key: win32-gocache + key: win32-qt5-gocache - name: Win32 docker build 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 with: path: ~/.cache/go-build - key: win64-gocache + key: win64-qt5-gocache - name: Win64 docker build 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 with: path: ~/.cache/go-build - key: win64-gocache + key: win64-qt68-gocache - name: Win64 docker build 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 with: path: ~/.cache/go-build - key: android-armv8a-gocache + key: android-qt5-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/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 From 99e6bd2a877710ff9402c10da81c60f63100aa1f Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 17:13:24 +1300 Subject: [PATCH 09/10] github/ci: add test for android qt6 build --- .github/workflows/miqt.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/miqt.yml b/.github/workflows/miqt.yml index 9d90ff34..313794ee 100644 --- a/.github/workflows/miqt.yml +++ b/.github/workflows/miqt.yml @@ -170,3 +170,34 @@ jobs: - name: Verify that package exists 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 From d6ad680e2864b2e69ad378e3f08901ae97557f0c Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 15 Feb 2025 18:20:49 +1300 Subject: [PATCH 10/10] doc/README: update docker instructions for Qt6 android build --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cf824de..d335adeb 100644 --- a/README.md +++ b/README.md @@ -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 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. ![](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"`. - Check `examples/android` to see how to support both Android and desktop platforms. 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: - `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: - - `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. 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`