Merge pull request #34 from mappu/android-support

Android support
This commit is contained in:
mappu 2024-09-24 19:30:58 +12:00 committed by GitHub
commit 8272d8a43d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1155 additions and 8 deletions

8
.gitignore vendored
View File

@ -1,5 +1,5 @@
# scratch files
*.json
# cache files for genbindings
cmd/genbindings/cachedir/
# docker files
container-build-cache/
@ -16,3 +16,7 @@ examples/mdoutliner/mdoutliner
examples/mdoutliner/mdoutliner.exe
examples/windowsmanifest/windowsmanifest
examples/windowsmanifest/windowsmanifest.exe
# android temporary build files
android-build
deployment-settings.json

View File

@ -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<br>- Tested with Debian 12 / Qt 5.15 / GCC 12
|Windows|Static, Dynamic (.dll)|✅ Works<br>- Tested with MXE Qt 5.15 / MXE GCC 5 under cross-compilation<br>- Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation
|Linux x86_64|Static, Dynamic (.so)|✅ Works<br>- Tested with Debian 12 / Qt 5.15 / GCC 12
|Windows x86_64|Static, Dynamic (.dll)|✅ Works<br>- Tested with MXE Qt 5.15 / MXE GCC 5 under cross-compilation<br>- Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation
|Android ARM64|Dynamic bundled in package|✅ Works<br>- 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 "-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`
- 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.

View File

@ -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}" <<EOF
{
"_description": "Generated by miqt/android-mktemplate",
"application-binary": "${ARG_APPNAME}",
"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-min-sdk-version": "23",
"android-target-sdk-version": "30",
"ndk": "/opt/android-sdk/ndk/22.1.7171670",
"ndk-host": "linux-x86_64",
"qt": "${QT_PATH}",
"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/",
"tool-prefix": "llvm",
"toolchain-prefix": "llvm",
"useLLVM": true
}
EOF
}
main "$@"

View File

@ -0,0 +1,84 @@
#!/bin/bash
#
# android-stub-gen generates a .so that runs an exported function from another
# so file. This is because Qt for Android always tries to run `main`, but Go's
# c-shared build mode cannot export a function named `main`.
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
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 <<EOF
#include <android/log.h>
#include <dlfcn.h>
#include <stdlib.h>
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 "$@"

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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

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

@ -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!")
}

BIN
examples/android/screenshot.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 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

@ -160,7 +160,7 @@ func (t *AppTab) updateOutlineForContent(content string) {
bookmark := qt.NewQListWidgetItem7(line, t.outline)
bookmark.SetToolTip(fmt.Sprintf("Line %d", lineNumber+1))
bookmark.SetData(lineNumberRole, qt.NewQVariant5(lineNumber))
bookmark.SetData(lineNumberRole, qt.NewQVariant7(lineNumber))
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 20 KiB

9
qt/cflags_android.go Normal file
View File

@ -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"

View File

@ -1,3 +1,5 @@
// +build linux,!android
package qt
/*