diff --git a/.github/workflows/miqt.yml b/.github/workflows/miqt.yml index 00088cc3..313794ee 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 @@ -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 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` 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." } 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) + ` /* 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 [] 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" 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/screenshot.png b/examples/android6/screenshot.png new file mode 100644 index 00000000..f5def8a6 Binary files /dev/null and b/examples/android6/screenshot.png differ 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() +} 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 /*