diff --git a/.gitignore b/.gitignore
index 9150b6b0..2bd04bf5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,6 @@ examples/helloworld/helloworld.exe
examples/mdoutliner/mdoutliner
examples/mdoutliner/mdoutliner.exe
examples/windowsmanifest/windowsmanifest
-examples/windowsmanifest/windowsmanifest.exe
\ No newline at end of file
+examples/windowsmanifest/windowsmanifest.exe
+
+android-build
\ No newline at end of file
diff --git a/README.md b/README.md
index 0d00b52e..20ef537d 100644
--- a/README.md
+++ b/README.md
@@ -19,8 +19,9 @@ These bindings were newly started in August 2024. The bindings are functional fo
|Platform|Linkage|Status
|---|---|---
-|Linux|Static, Dynamic (.so)|✅ Works
- Tested with Debian 12 / Qt 5.15 / GCC 12
-|Windows|Static, Dynamic (.dll)|✅ Works
- Tested with MXE Qt 5.15 / MXE GCC 5 under cross-compilation
- Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation
+|Linux x86_64|Static, Dynamic (.so)|✅ Works
- Tested with Debian 12 / Qt 5.15 / GCC 12
+|Windows x86_64|Static, Dynamic (.dll)|✅ Works
- Tested with MXE Qt 5.15 / MXE GCC 5 under cross-compilation
- Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation
+|Android ARM64|Dynamic bundled in package|✅ Works
- Tested with Raymii Qt 5.15 / Android SDK 31 / Android NDK 22
|macOS x86_64|Static, Dynamic (.dylib)|Should work, [not tested](https://github.com/mappu/miqt/issues/2)
|macOS ARM64|Static, Dynamic (.dylib)|[Blocked by #11](https://github.com/mappu/miqt/issues/11)
@@ -44,7 +45,11 @@ Yes. You must also meet your Qt license obligations: either use Qt dynamically-l
### Q3. Why does it take so long to compile?
-The first time the Qt bindings are compiled takes a long time. After this, it's fast. In a Dockerfile, you could cache the build step by running `go install github.com/mappu/miqt`.
+The first time the Qt bindings are compiled takes a long time. After this, it's fast.
+
+If you are compiling your app within a Dockerfile, you could cache the build step by running `go install github.com/mappu/miqt`.
+
+If you are compiling your app with a `docker run` command, the compile speed can be improved if you also bind-mount the Docker container's `GOCACHE` directory: `-v $(pwd)/container-build-cache:/root/.cache/go-build`
See also [issue #8](https://github.com/mappu/miqt/issues/8).
@@ -91,7 +96,7 @@ For dynamically-linked builds (closed-source or open source application):
- `docker run --rm -v $(pwd):/src -w /src miqt/win64-dynamic:latest go build -buildvcs=false -ldflags '-s -w -H windowsgui'`
3. Copy necessary Qt LGPL libraries and plugin files.
-For repeated builds, the compile speed can be improved if you also bind-mount the Docker container's `GOCACHE` directory: `-v $(pwd)/container-build-cache:/root/.cache/go-build`
+See Q3 for advice about docker performance.
To add an icon and other properties to the .exe, you can use [the go-winres tool](https://github.com/tc-hib/go-winres). See the `examples/windowsmanifest` for details.
@@ -114,3 +119,33 @@ $env:CGO_CXXFLAGS = '-Wno-ignored-attributes -D_Bool=bool' # Clang 18 recommenda
```
4. Run `go build -ldflags "-s -w -H windowsgui"`
+
+### Q9. How can I compile for Android?
+
+Miqt supports compiling for Android. Some extra steps are required to bridge the Java, C++, Go worlds.
+
+![](doc/android-architecture.png)
+
+1. Modify your main function to [support `c-shared` build mode](https://pkg.go.dev/cmd/go#hdr-Build_modes).
+ - Package `main` must have an empty `main` function.
+ - Rename your `main` function to `AndroidMain` and add a comment `//export AndroidMain`.
+ - 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 android-armv8a-go1.23-qt5.15-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 "-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`
+ - 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`
+6. Build the android package:
+ - `docker run --rm -v $(pwd):/src -w /src miqt/android:latest androiddeployqt --input ./deployment-settings.json --output ./android-build/`
+ - By default, the resulting `.apk` is generated at `android-build/build/outputs/apk/debug/android-build-debug.apk`.
+ - You can build in release mode by adding `--release`
+
+See Q3 for advice about docker performance.
+
+For repeated builds, if you customize the `AndroidManifest.xml` file or images, they will be used for the next `androiddeployqt` run.
+
diff --git a/cmd/android-mktemplate/android-mktemplate.sh b/cmd/android-mktemplate/android-mktemplate.sh
new file mode 100755
index 00000000..d42e2587
--- /dev/null
+++ b/cmd/android-mktemplate/android-mktemplate.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+#
+# android-mktemplate generates a template json file suitable for use with the
+# androiddeployqt tool.
+
+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 2 ]] ; then
+ echo "Usage: android-mktemplate.sh appname output.json" >&2
+ exit 1
+ fi
+ local ARG_APPNAME="$1"
+ local ARG_DESTFILE="$2"
+
+ # Available fields are documented in the template file at
+ # @ref /usr/local/Qt-5.15.13/mkspecs/features/android/android_deployment_settings.prf
+ cat > "${ARG_DESTFILE}" <&2
+ exit 1
+ fi
+ local ARG_SOURCE_SOFILE="$1"
+ local ARG_FUNCTIONNAME="$2"
+ local ARG_DEST_SOFILE="$3"
+
+ local tmpdir=$(mktemp -d)
+ trap "rm -r ${tmpdir}" EXIT
+
+ echo "- Using temporary directory: ${tmpdir}"
+ echo "- Found Qt path: ${QT_PATH}"
+
+ echo "Generating stub..."
+
+ cat > $tmpdir/miqtstub.cpp <
+#include
+#include
+
+typedef void goMainFunc_t();
+
+int main(int argc, char** argv) {
+ __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "Starting up");
+
+ void* handle = dlopen("$(basename "$ARG_SOURCE_SOFILE")", RTLD_LAZY);
+ if (handle == NULL) {
+ __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: null handle opening so: %s", dlerror());
+ exit(1);
+ }
+
+ void* goMain = dlsym(handle, "${ARG_FUNCTIONNAME}");
+ if (goMain == NULL) {
+ __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: null handle looking for function: %s", dlerror());
+ exit(1);
+ }
+
+ __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: Found target, calling");
+
+ // Cast to function pointer and call
+ goMainFunc_t* f = (goMainFunc_t*)goMain;
+ f();
+
+ __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: Target function returned");
+ return 0;
+}
+
+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"
+
+
+ echo "Done."
+}
+
+main "$@"
diff --git a/doc/android-architecture.png b/doc/android-architecture.png
new file mode 100644
index 00000000..ac45c759
Binary files /dev/null and b/doc/android-architecture.png differ
diff --git a/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile
new file mode 100644
index 00000000..5f8f3288
--- /dev/null
+++ b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile
@@ -0,0 +1,17 @@
+FROM raymii/qt:5.15-android-source
+
+RUN wget 'https://go.dev/dl/go1.23.1.linux-amd64.tar.gz' && \
+ tar x -C /usr/local/ -f go1.23.1.linux-amd64.tar.gz && \
+ rm go1.23.1.linux-amd64.tar.gz
+
+COPY ../cmd/android-stub-gen/android-stub-gen.sh /usr/local/bin/android-stub-gen.sh
+COPY ../cmd/android-stub-gen/android-mktemplate.sh /usr/local/bin/android-mktemplate.sh
+
+ENV PATH=/usr/local/go/bin:/opt/cmake/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/Qt-5.15.13/bin:/opt/android-sdk/cmdline-tools/tools/bin:/opt/android-sdk/tools:/opt/android-sdk/tools/bin:/opt/android-sdk/platform-tools
+
+ENV CC=/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang
+ENV CXX=/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++
+ENV CGO_ENABLED=1
+ENV GOOS=android
+ENV GOARCH=arm64
+ENV GOFLAGS=-buildvcs=false
\ No newline at end of file
diff --git a/examples/android/main.go b/examples/android/main.go
new file mode 100644
index 00000000..fed6cfdc
--- /dev/null
+++ b/examples/android/main.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/mappu/miqt/qt"
+)
+
+func myRealMainFunc() {
+
+ qt.NewQApplication(os.Args)
+
+ btn := qt.NewQPushButton2("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/android/screenshot.png b/examples/android/screenshot.png
new file mode 100755
index 00000000..9ea1170c
Binary files /dev/null and b/examples/android/screenshot.png differ
diff --git a/examples/android/startup_android.go b/examples/android/startup_android.go
new file mode 100644
index 00000000..2fbc4d5b
--- /dev/null
+++ b/examples/android/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/android/startup_other.go b/examples/android/startup_other.go
new file mode 100644
index 00000000..d2382476
--- /dev/null
+++ b/examples/android/startup_other.go
@@ -0,0 +1,7 @@
+// +build !android
+
+package main
+
+func main() {
+ myRealMainFunc()
+}
diff --git a/qt/cflags_android.go b/qt/cflags_android.go
new file mode 100644
index 00000000..7103b856
--- /dev/null
+++ b/qt/cflags_android.go
@@ -0,0 +1,9 @@
+package qt
+
+/*
+
+#cgo CXXFLAGS: -fPIC -DQT_WIDGETS_LIB -I/usr/local/Qt-5.15.13/include/QtWidgets -I/usr/local/Qt-5.15.13/include/ -I/usr/local/Qt-5.15.13/include/QtCore -DQT_GUI_LIB -I/usr/local/Qt-5.15.13/include/QtGui -DQT_CORE_LIB
+#cgo LDFLAGS: /usr/local/Qt-5.15.13/lib/libQt5Widgets_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5Gui_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5Core_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5Svg_arm64-v8a.so /usr/local/Qt-5.15.13/lib/libQt5AndroidExtras_arm64-v8a.so
+
+*/
+import "C"
diff --git a/qt/cflags_linux.go b/qt/cflags_linux.go
index 7fd2cbdd..51e34c25 100644
--- a/qt/cflags_linux.go
+++ b/qt/cflags_linux.go
@@ -1,3 +1,5 @@
+// +build linux,!android
+
package qt
/*