Skip to content

Commit

Permalink
miqt: android support
Browse files Browse the repository at this point in the history
  • Loading branch information
mappu committed Sep 24, 2024
1 parent b0fe164 commit 239ed88
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 5 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ examples/helloworld/helloworld.exe
examples/mdoutliner/mdoutliner
examples/mdoutliner/mdoutliner.exe
examples/windowsmanifest/windowsmanifest
examples/windowsmanifest/windowsmanifest.exe
examples/windowsmanifest/windowsmanifest.exe

android-build
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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).

Expand Down Expand Up @@ -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.

Expand All @@ -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.

47 changes: 47 additions & 0 deletions cmd/android-mktemplate/android-mktemplate.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
84 changes: 84 additions & 0 deletions cmd/android-stub-gen/android-stub-gen.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
Binary file added doc/android-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions examples/android/main.go
Original file line number Diff line number Diff line change
@@ -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!")
}
Binary file added examples/android/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions examples/android/startup_android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// +build android

package main

import "C" // Required for export support

//export AndroidMain
func AndroidMain() {
myRealMainFunc()
}

func main() {
// Must be empty
}
7 changes: 7 additions & 0 deletions examples/android/startup_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !android

package main

func main() {
myRealMainFunc()
}
9 changes: 9 additions & 0 deletions qt/cflags_android.go
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions qt/cflags_linux.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux,!android

package qt

/*
Expand Down

0 comments on commit 239ed88

Please sign in to comment.