diff --git a/.gitignore b/.gitignore index 9150b6b..2bd04bf 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 0d00b52..20ef537 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 0000000..d42e258 --- /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 0000000..ac45c75 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 0000000..5f8f328 --- /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 0000000..fed6cfd --- /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 0000000..9ea1170 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 0000000..2fbc4d5 --- /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 0000000..d238247 --- /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 0000000..7103b85 --- /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 7fd2cbd..51e34c2 100644 --- a/qt/cflags_linux.go +++ b/qt/cflags_linux.go @@ -1,3 +1,5 @@ +// +build linux,!android + package qt /*