Merge pull request #208 from mappu/miqt-docker-cmd

Add miqt-docker helper command, add one-command Android builds
This commit is contained in:
mappu 2025-05-01 22:41:32 +12:00 committed by GitHub
commit 9d4cabc099
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1308 additions and 545 deletions

View File

@ -1,33 +0,0 @@
cmd/genbindings/cachedir
*.exe
cmd/handbindings/handbindings
cmd/handbindings/bindings_test/direct
cmd/handbindings/bindings_test/testapp
cmd/genbindings/genbindings
cmd/miqt-uic/miqt-uic
cmd/miqt-rcc/miqt-rcc
examples/goroutine6/goroutine6
examples/helloworld/helloworld
examples/helloworld6/helloworld6
examples/mdoutliner/mdoutliner
examples/mdoutliner6/mdoutliner6
examples/windowsmanifest/windowsmanifest
examples/uidesigner/uidesigner
examples/trivialwizard6/trivialwizard6
examples/subclass/subclass
examples/modelview/modelview
examples/modelview_color6/modelview_color6
examples/libraries/extras-scintillaedit/extras-scintillaedit
examples/libraries/qt-multimedia/qt-multimedia
examples/libraries/qt-network/qt-network
examples/libraries/qt-printsupport/qt-printsupport
examples/libraries/qt-script/qt-script
examples/libraries/qt-svg/qt-svg
examples/libraries/qt-webengine/qt-webengine
examples/libraries/qt-webkit/qt-webkit
examples/libraries/qt6-multimedia/qt6-multimedia
examples/libraries/qt6-webengine/qt6-webengine
examples/libraries/restricted-extras-qscintilla/restricted-extras-qscintilla

View File

@ -14,6 +14,9 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# WARNING: This loads the cache but does not commit back any new changes to
# it unless the cache is invalidated in GitHub
# It will help to do that every time a new Qt library is added
- name: Cache clang ASTs - name: Cache clang ASTs
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -33,15 +36,13 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache GOCACHE # This uses the `genbindings` container in miqt-docker
uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: linux64-buildall-gocache
- name: Rebuild all libraries and examples - name: Rebuild all libraries and examples
run: make build-all run: make build-all
- name: Run marshalling test suite
run: cmd/miqt-docker/miqt-docker genbindings /bin/bash -c 'cd examples/marshalling && env QT_QPA_PLATFORM=offscreen go test -v'
miqt_linux64_qt5: miqt_linux64_qt5:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -49,17 +50,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Linux64 docker build - name: Linux64 bindings compile
run: docker build -t miqt/linux64:qt5 -f docker/linux64-go1.19-qt5.15-dynamic.Dockerfile . run: |
make cmd/miqt-docker/miqt-docker
- name: Cache GOCACHE cd qt && ../cmd/miqt-docker/miqt-docker linux64-go1.19-qt5.15-dynamic go build
uses: actions/cache@v4
with:
path: ~/.cache/go-build
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'
miqt_linux64_qt6_4: miqt_linux64_qt6_4:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -68,17 +62,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Linux64 docker build
run: docker build -t miqt/linux64:qt64 -f docker/linux64-go1.19-qt6.4-dynamic.Dockerfile .
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: linux64-qt64-gocache
- name: Linux64 bindings compile - name: Linux64 bindings compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/linux64:qt64 /bin/bash -c 'cd qt6 && go build' run: |
make cmd/miqt-docker/miqt-docker
cd qt6 && ../cmd/miqt-docker/miqt-docker linux64-go1.19-qt6.4-dynamic go build
miqt_linux64_qt6_8: miqt_linux64_qt6_8:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -87,17 +74,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Linux64 docker build
run: docker build -t miqt/linux64:qt68 -f docker/linux64-go1.23-qt6.8-dynamic.Dockerfile .
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: linux64-qt68-gocache
- name: Linux64 bindings compile - name: Linux64 bindings compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/linux64:qt68 /bin/bash -c 'cd qt6 && go build' run: |
make cmd/miqt-docker/miqt-docker
cd qt6 && ../cmd/miqt-docker/miqt-docker linux64-go1.23-qt6.8-dynamic go build
miqt_win32_qt5: miqt_win32_qt5:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -106,17 +86,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
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 .
- name: Win32 bindings compile - name: Win32 bindings compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/win32:qt5 /bin/bash -c 'cd qt && go build && cd ../examples/helloworld && go build' run: |
make cmd/miqt-docker/miqt-docker
cmd/miqt-docker/miqt-docker win32-cross-go1.23-qt5.15-static /bin/bash -c 'cd qt && go build && cd ../examples/helloworld && go build'
miqt_win64_qt5: miqt_win64_qt5:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -125,17 +98,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
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 .
- name: Win64 bindings compile - name: Win64 bindings compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/win64:qt5 /bin/bash -c 'cd qt && go build && cd ../examples/helloworld && go build' run: |
make cmd/miqt-docker/miqt-docker
cmd/miqt-docker/miqt-docker win64-cross-go1.23-qt5.15-static /bin/bash -c 'cd qt && go build && cd ../examples/helloworld && go build'
miqt_win64_qt68: miqt_win64_qt68:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -144,17 +110,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache GOCACHE
uses: actions/cache@v4
with:
path: ~/.cache/go-build
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 .
- name: Win64 bindings compile - name: Win64 bindings compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src miqt/win64:qt68 /bin/bash -c 'cd qt6 && go build && cd ../examples/helloworld6 && go build' run: |
make cmd/miqt-docker/miqt-docker
cmd/miqt-docker/miqt-docker win64-cross-go1.23-qt6.8-dynamic /bin/bash -c 'cd qt6 && go build && cd ../examples/helloworld6 && go build'
miqt_android_qt5: miqt_android_qt5:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -163,57 +122,23 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Android armv8a docker build - name: Android compile
run: docker build -t miqt/android:qt5 -f docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile . run: |
make cmd/miqt-docker/miqt-docker
cd examples/helloworld
../../cmd/miqt-docker/miqt-docker android-armv8a-go1.23-qt5.15-dynamic -android-build
test -f helloworld.apk
- name: Cache GOCACHE miqt_android_qt6:
uses: actions/cache@v4 runs-on: ubuntu-24.04
with:
path: ~/.cache/go-build
key: android-qt5-armv8a-gocache
- name: Android compile app as c-shared my_go_app.so steps:
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 - name: Checkout
uses: actions/checkout@v4
- name: Android generate libRealAppName.so linking stub - name: Android compile
run: docker run -v ~/.cache/go-build:/root/.cache/go-build -v $PWD:/src -w /src/examples/android miqt/android:qt5 android-stub-gen.sh my_go_app.so AndroidMain android-build/libs/arm64-v8a/libRealAppName_arm64-v8a.so run: |
make cmd/miqt-docker/miqt-docker
- name: Android generate json packaging metadata cd examples/helloworld6
run: docker run --rm -v $(pwd):/src -w /src/examples/android miqt/android:qt5 android-mktemplate.sh RealAppName deployment-settings.json ../../cmd/miqt-docker/miqt-docker android-armv8a-go1.23-qt6.6-dynamic -android-build
test -f helloworld6.apk
- name: Android build APK package
run: docker run --rm -v $(pwd):/src -w /src/examples/android miqt/android:qt5 androiddeployqt --input ./deployment-settings.json --output ./android-build/
- 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

10
.gitignore vendored
View File

@ -1,15 +1,6 @@
# cache files for genbindings # cache files for genbindings
cmd/genbindings/cachedir/ cmd/genbindings/cachedir/
# docker files
container-build-cache/
# local genbindings configuration
cmd/genbindings/genbindings.local*
# local pkg-config configuration
pkg-config/*.pc
# binaries # binaries
*.exe *.exe
@ -17,6 +8,7 @@ cmd/handbindings/handbindings
cmd/handbindings/bindings_test/direct cmd/handbindings/bindings_test/direct
cmd/handbindings/bindings_test/testapp cmd/handbindings/bindings_test/testapp
cmd/genbindings/genbindings cmd/genbindings/genbindings
cmd/miqt-docker/miqt-docker
cmd/miqt-uic/miqt-uic cmd/miqt-uic/miqt-uic
cmd/miqt-rcc/miqt-rcc cmd/miqt-rcc/miqt-rcc

View File

@ -1,40 +1,23 @@
BUILDSTAMPS := docker/genbindings.docker-buildstamp
DOCKER := docker
SHELL := /bin/bash SHELL := /bin/bash
GO := go
# DOCKEREXEC runs the target command in the `genbindings` docker container.
# It mounts in the current GOCACHE and GOMODCACHE.
DOCKEREXEC = mkdir -p "$$(go env GOCACHE)" && \
mkdir -p "$$(go env GOMODCACHE)" && \
$(DOCKER) run \
--user "$$(id -u):$$(id -g)" \
-v "$$(go env GOCACHE):/.cache/go-build" \
-v "$$(go env GOMODCACHE):/go/pkg/mod" \
-v "$$PWD:/src" \
-w /src \
miqt/genbindings:latest \
/bin/bash -c
.PHONY: all .PHONY: all
all: genbindings all: genbindings
docker/genbindings.docker-buildstamp: docker/genbindings.Dockerfile cmd/miqt-docker/miqt-docker: go.mod cmd/miqt-docker/*.go docker/*.Dockerfile
$(DOCKER) build -t miqt/genbindings:latest -f docker/genbindings.Dockerfile . $(GO) build -o cmd/miqt-docker/miqt-docker ./cmd/miqt-docker
touch $@
.PHONY: clean
clean:
$(DOCKER) image rm -f miqt/genbindings:latest
rm -f $(BUILDSTAMPS)
.PHONY: clean-cache .PHONY: clean-cache
clean-cache: clean-cache:
rm -f cmd/genbindings/cachedir/*.json rm -f cmd/genbindings/cachedir/*.json
cmd/genbindings/genbindings: go.mod cmd/genbindings/*.go
$(GO) build -o cmd/genbindings/genbindings ./cmd/genbindings
.PHONY: genbindings .PHONY: genbindings
genbindings: $(BUILDSTAMPS) genbindings: cmd/miqt-docker/miqt-docker cmd/genbindings/genbindings
$(DOCKEREXEC) 'cd cmd/genbindings && go build && ./genbindings' cd cmd/genbindings && ../miqt-docker/miqt-docker genbindings ./genbindings
.PHONY: build-all .PHONY: build-all
build-all: $(BUILDSTAMPS) build-all: cmd/miqt-docker/miqt-docker
$(DOCKEREXEC) 'go build ./...' ./cmd/miqt-docker/miqt-docker genbindings go build ./...

View File

@ -53,6 +53,8 @@ Make sure to compile with `go build -ldflags "-s -w"`. This reduces the `hellowo
Then, it's possible to reduce the size further with `upx --best` to 2MB or `upx --lzma` to 1.4MB. Then, it's possible to reduce the size further with `upx --best` to 2MB or `upx --lzma` to 1.4MB.
You can also try `miqt-docker native -minify-build` to use aggressive `CFLAGS`.
### Q2. Can I release a proprietary, commercial app with this binding? ### Q2. Can I release a proprietary, commercial app with this binding?
Yes. You must also meet your Qt license obligations: either use Qt dynamically-linked dll/so/dylib files under the LGPL, or, purchase a Qt commercial license for static linking. Yes. You must also meet your Qt license obligations: either use Qt dynamically-linked dll/so/dylib files under the LGPL, or, purchase a Qt commercial license for static linking.
@ -63,7 +65,7 @@ The first time MIQT is used, your `go build` would take [about 10 minutes](https
If you are compiling your app within a Dockerfile, you could cache the build step by running `go install github.com/mappu/miqt/qt`. If you are compiling your app within a Dockerfile, you could cache the build step by running `go install github.com/mappu/miqt/qt`.
If you are compiling your app with a one-shot `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` If you are compiling your app with a one-shot `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`. The `miqt-docker` helper app does this automatically.
See also [issue #8](https://github.com/mappu/miqt/issues/8). See also [issue #8](https://github.com/mappu/miqt/issues/8).
@ -84,27 +86,29 @@ MIQT is a clean-room binding that does not use any code from other Qt bindings.
Most functions are implemented 1:1. [The Qt documentation](https://doc.qt.io/qt-5/classes.html) should be used. Most functions are implemented 1:1. [The Qt documentation](https://doc.qt.io/qt-5/classes.html) should be used.
The `QByteArray`, `QString`, `QList<T>`, `QVector<T>`, `QMap<K,V>`, `QHash<K,V>` types are projected as plain Go `[]byte`, `string`, `[]T`, and `map[K]V`. Therefore, you can't call any of the Qt type's methods, you must use some Go equivalent method instead. Container types:
- The `QByteArray`, `QString`, `QList<T>`, `QVector<T>`, `QMap<K,V>`, `QHash<K,V>` types are projected as plain Go `[]byte`, `string`, `[]T`, and `map[K]V`. Therefore, you can't call any of the Qt type's methods, you must use some Go equivalent method instead.
- Go strings are internally converted to QString using `QString::fromUtf8`. Therefore, the Go string must be UTF-8 to avoid [mojibake](https://en.wikipedia.org/wiki/Mojibake). If the Go string contains binary data, the conversion would corrupt such bytes into U+FFFD (<28>). On return to Go space, this becomes `\xEF\xBF\xBD`. - Go strings are internally converted to QString using `QString::fromUtf8`. Therefore, the Go string must be UTF-8 to avoid [mojibake](https://en.wikipedia.org/wiki/Mojibake). If the Go string contains binary data, the conversion would corrupt such bytes into U+FFFD (<28>). On return to Go space, this becomes `\xEF\xBF\xBD`.
- The iteration order of a Qt `QMap`/`QHash` will differ from the Go map iteration order. `QMap` is iterated by key order, but Go maps and `QHash` iterate in an undefined internal order.
- The iteration order of a Qt QMap/QHash will differ from the Go map iteration order. QMap is iterated by key order, but Go maps and QHash iterate in an undefined internal order. Memory management:
- Where Qt returns a C++ object by value (e.g. `QSize`), the binding may have moved it to the heap, and in Go this may be represented as a pointer type. In such cases, a Go finalizer is added to automatically delete the heap object. This means code using MIQT can look basically similar to the Qt C++ equivalent code.
Where Qt returns a C++ object by value (e.g. `QSize`), the binding may have moved it to the heap, and in Go this may be represented as a pointer type. In such cases, a Go finalizer is added to automatically delete the heap object. This means code using MIQT can look basically similar to the Qt C++ equivalent code.
The `connect(sourceObject, sourceSignal, targetObject, targetSlot)` is projected as `targetObject.onSourceSignal(func()...)`.
Events and signals:
- The `connect(sourceObject, sourceSignal, targetObject, targetSlot)` is projected as `targetObject.onSourceSignal(func()...)`.
- You can also override virtual methods like PaintEvent in the same way. Your callback `func()` receives `super()` as a first argument that can be used to call the base class implementation. - You can also override virtual methods like PaintEvent in the same way. Your callback `func()` receives `super()` as a first argument that can be used to call the base class implementation.
Qt class inherited types are projected as a Go embedded struct. For example, to pass a `var myLabel *qt.QLabel` to a function taking only the `*qt.QWidget` base class, write `myLabel.QWidget`. Class pointers:
- Qt class inherited types are projected as a Go embedded struct. For example, to pass a `var myLabel *qt.QLabel` to a function taking only the `*qt.QWidget` base class, write `myLabel.QWidget`.
- When a Qt subclass adds a method overload (e.g. `QMenu::addAction(QString)` vs `QWidget::addAction(QAction*)`), the base class version is shadowed and can only be called via `myQMenu.QWidget.AddAction(QAction*)`. - When a Qt subclass adds a method overload (e.g. `QMenu::addAction(QString)` vs `QWidget::addAction(QAction*)`), the base class version is shadowed and can only be called via `myQMenu.QWidget.AddAction(QAction*)`.
- A MIQT pointer points to a Go struct, not to the raw C++ Qt widget class. Therefore `QTabWidget.CurrentWidget() == MyTab` will never compare equal because `CurrentWidget()` created a new Go struct wrapping the same C++ pointer. You can compare `QTabWidget.CurrentIndex()`, or, you can use: `QTabWidget.CurrentWidget().UnsafePointer() == MyTab.UnsafePointer()`. - A MIQT pointer points to a Go struct, not to the raw C++ Qt widget class. Therefore `QTabWidget.CurrentWidget() == MyTab` will never compare equal because `CurrentWidget()` created a new Go struct wrapping the same C++ pointer. You can compare `QTabWidget.CurrentIndex()`, or, you can use: `QTabWidget.CurrentWidget().UnsafePointer() == MyTab.UnsafePointer()`.
The Go runtime migrates goroutines between OS threads, but Qt expects fixed OS threads to be used for each QObject. When you first call `qt.NewQApplication` in MIQT, that will be considered the [Qt main thread](https://doc.qt.io/qt-6/thread-basics.html#gui-thread-and-worker-thread) and will automatically signal the Go runtime to bind to a fixed OS thread using `runtime.LockOSThread()`. Multithreading:
- The Go runtime migrates goroutines between OS threads, but Qt expects fixed OS threads to be used for each QObject. When you first call `qt.NewQApplication` in MIQT, that will be considered the [Qt main thread](https://doc.qt.io/qt-6/thread-basics.html#gui-thread-and-worker-thread) and will automatically signal the Go runtime to bind to a fixed OS thread using `runtime.LockOSThread()`.
- When accessing Qt objects from inside another goroutine, it's safest to use `(qt6/mainthread).Wait()` or `Start()` to access the Qt objects from Qt's main thread.
- When accessing Qt objects from inside another goroutine, it's safest to use `(qt6/mainthread).Wait()` to access the Qt objects from Qt's main thread. Android:
- A `QFileDialog` may return a filepath of the form `content://...`. Such paths can be opened with `qt.QFile` but not with Go `os.Open()`; you can pass the handle to Go using `os.NewFile(QFile.Handle(), "name")`.
Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future. Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future.
@ -118,20 +122,22 @@ MIQT has a custom implementation of Qt `uic` and `rcc` tools, to allow using [Qt
MIQT uses `pkg-config` to find all used Qt libraries. Every Qt library should have a definition file in `.pc` format, which provides CGO with the necessary `CXXFLAGS`/`LDFLAGS`. Your Qt development environment already included the necessary `.pc` definition files. MIQT uses `pkg-config` to find all used Qt libraries. Every Qt library should have a definition file in `.pc` format, which provides CGO with the necessary `CXXFLAGS`/`LDFLAGS`. Your Qt development environment already included the necessary `.pc` definition files.
You can use the `PKG_CONFIG_PATH` environment variable to override where CGO looks for `.pc` files. [Read more »](pkg-config/README.md) You can use the `PKG_CONFIG_PATH` environment variable to override where CGO looks for `.pc` files. [Read more »](doc/pkg-config.md)
### Q8. How can I upgrade a MIQT app from Qt 5 to Qt 6? ### Q8. How can I upgrade a MIQT app from Qt 5 to Qt 6?
The import path changes from `github.com/mappu/miqt/qt` to `github.com/mappu/miqt/qt6`, but most basic classes are the same. The import path changes from `github.com/mappu/miqt/qt` to `github.com/mappu/miqt/qt6`, but most basic classes are the same.
You can replace the import path in two ways: You can update all imports by running `find . -type f -name .go -exec sed -i 's_"github.com/mappu/miqt/qt"_qt "github.com/mappu/miqt/qt6"_' {} \;`
1. Add a go.mod directive: Run `go mod edit -replace github.com/mappu/miqt/qt=github.com/mappu/miqt/qt6`
2. Or, update all imports: Run `find . -type f -name .go -exec sed -i 's_"github.com/mappu/miqt/qt"_qt "github.com/mappu/miqt/qt6"_' {} \;`
### Q9. How can I add bindings for another Qt library? ### Q9. How can I add bindings for another Qt library?
Fork this repository and add your library to the `genbindings/config-libraries` file. [Read more »](cmd/genbindings/README.md) Fork this repository and add your library to the `genbindings/config-libraries` file. [Read more »](cmd/genbindings/README.md)
### Q10. Is there an easy build tool?
You can use the ordinary `go get` and `go build` commands. To help with cross-compilation, you can use the optional `miqt-docker` tool. [Read more »](cmd/miqt-docker/README.md)
## Building ## Building
### Linux (native) ### Linux (native)
@ -180,6 +186,13 @@ pacman -S pkg-config gcc go qt6-base qscintilla-qt6 qt6-charts qt6-multimedia qt
go build -ldflags '-s -w' go build -ldflags '-s -w'
``` ```
### Windows (Docker with miqt-docker)
```bash
go install github.com/mappu/miqt/cmd/miqt-docker
miqt-docker win64-qt6-static -windows-build # or -qt5- or -static
```
### Windows (native) ### Windows (native)
*Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation* *Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation*
@ -236,14 +249,14 @@ Static linking is also available by installing the `mingw-w64-ucrt-x86_64-qt5-st
For static linking: For static linking:
1. Build the necessary docker container for cross-compilation: 1. Build the necessary docker container for cross-compilation:
- `docker build -t miqt/win64-cross:latest -f docker/win64-cross-go1.23-qt5.15-static.Dockerfile .` - In the `docker/` directory: docker build -t miqt/win64-cross:latest -f win64-cross-go1.23-qt5.15-static.Dockerfile .`
2. Build your application: 2. Build your application:
- `docker run --rm -v $(pwd):/src -w /src miqt/win64-cross:latest go build --tags=windowsqtstatic -ldflags '-s -w -H windowsgui'` - `docker run --rm -v $(pwd):/src -w /src miqt/win64-cross:latest go build --tags=windowsqtstatic -ldflags '-s -w -H windowsgui'`
For dynamic linking: For dynamic linking:
1. Build the necessary docker container for cross-compilation: 1. Build the necessary docker container for cross-compilation:
- `docker build -t miqt/win64-dynamic:latest -f docker/win64-cross-go1.23-qt5.15-dynamic.Dockerfile .` - In the `docker/` directory: `docker build -t miqt/win64-dynamic:latest -f win64-cross-go1.23-qt5.15-dynamic.Dockerfile .`
2. Build your application: 2. Build your application:
- `docker run --rm -v $(pwd):/src -w /src miqt/win64-dynamic:latest go build -ldflags '-s -w -H windowsgui'` - `docker run --rm -v $(pwd):/src -w /src miqt/win64-dynamic:latest go build -ldflags '-s -w -H windowsgui'`
3. Copy necessary Qt LGPL libraries and plugin files. 3. Copy necessary Qt LGPL libraries and plugin files.
@ -290,44 +303,26 @@ Installing `qt@5` from Homebrew may be very slow if Homebrew chooses to do a fro
For dynamic linking: For dynamic linking:
1. Build the necessary docker container for cross-compilation: 1. Build the necessary docker container for cross-compilation:
- `docker build -t miqt/osxcross:latest -f docker/macos-cross-x86_64-sdk14.5-go1.19-qt5.15-dynamic.Dockerfile .` - In the `docker/` directory: `docker build -t miqt/osxcross:latest -f macos-cross-x86_64-sdk14.5-go1.19-qt5.15-dynamic.Dockerfile .`
2. Build your application: 2. Build your application:
- `docker run --rm -v $(pwd):/src -w /src miqt/osxcross:latest go build -ldflags '-s -w'` - `docker run --rm -v $(pwd):/src -w /src miqt/osxcross:latest go build -ldflags '-s -w'`
3. Copy necessary Qt LGPL libraries and plugin files. 3. Copy necessary Qt LGPL libraries and plugin files.
See FAQ Q3 for advice about docker performance. See FAQ Q3 for advice about docker performance.
### Android (Docker) ### Android (Docker with miqt-docker)
*Tested with Raymii Qt 5.15 / Android SDK 31 / Android NDK 22* *Tested with Raymii Qt 5.15 / Android SDK 31 / Android NDK 22 and with Qt.io Qt 6.6 / Android SDK 33 / Android NDK 25*
*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. MIQT supports compiling for Android. Some extra steps are required to bridge the Java, C++, Go worlds.
![](doc/android-architecture.png) ![](doc/android-architecture.png)
1. Modify your main function to [support `c-shared` build mode](https://pkg.go.dev/cmd/go#hdr-Build_modes). ```bash
- Package `main` must have an empty `main` function. go install github.com/mappu/miqt/cmd/miqt-docker
- Rename your `main` function to `AndroidMain` and add a comment `//export AndroidMain`. miqt-docker android-qt5 -android-build # or android-qt6
- 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:
- (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:
- (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`
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 FAQ Q3 for advice about docker performance. This produces a `.apk` in the current directory. A default manifest, icon, and keystore will be created. If you customize the `AndroidManifest.xml` file or images, they will be used for the next build.
For repeated builds, only steps 3 and 6 are needed. If you customize the `AndroidManifest.xml` file or images, they will be used for the next `androiddeployqt` run. Advanced users may customize the build process or manually invoke `androiddeployqt`. You can invoke it inside the container environment via `miqt-docker android-qt5 androiddeployqt ...`. For more information, see the `android-build.sh` file that `miqt-docker` is running.

View File

@ -1,66 +0,0 @@
#!/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/}
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() {
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": "$(extra_libs)",
"android-min-sdk-version": "23",
"android-target-sdk-version": "$(target_sdk_version)",
"ndk": "/opt/android-sdk/ndk/$(ndk_version)",
"ndk-host": "linux-x86_64",
"qt": "${QT_ANDROID}",
"sdk": "/opt/android-sdk",
"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
}
EOF
}
main "$@"

View File

@ -1,106 +0,0 @@
#!/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
main() {
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 "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
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."
}
main "$@"

View File

@ -38,7 +38,7 @@ You should check the following configuration:
1. Git clone this repository 1. Git clone this repository
2. In `docker/genbindings.Dockerfile`, add your library's headers and pkg-config file. 2. In `docker/genbindings.Dockerfile`, add your library's headers and pkg-config file.
- If your library does not include a pkg-config file, [you must create one.](pkg-config/README.md) - If your library does not include a pkg-config file, [you must create one.](../../doc/pkg-config.md)
3. Patch `cmd/genbindings/config-libraries.go` to add a new `generate` block for your target library 3. Patch `cmd/genbindings/config-libraries.go` to add a new `generate` block for your target library
4. Run `genbindings` to regenerate all bindings 4. Run `genbindings` to regenerate all bindings
- The first run must populate clang ASTs into a cache directory and may be slower, but it is fast afterwards - The first run must populate clang ASTs into a cache directory and may be slower, but it is fast afterwards

63
cmd/miqt-docker/README.md Normal file
View File

@ -0,0 +1,63 @@
# miqt-docker
This is an optional helper program to quickly run a dockerized MIQT cross-compiler
environment. Many containers are available targeting different OSes and Qt versions.
It also has some built-in commands that can be run either dockerized or natively.
## Usage
Run `miqt-docker` with no arguments to see full usage instructions and all
available embedded dockerfiles:
```bash
Usage: miqt-docker ENVIRONMENT COMMAND...
COMMAND may be any shell command (e.g. go build); or /bin/bash to get an
interactive terminal; or one of the following predefined tasks:
-build Run 'go build' with usual MIQT flags
-minify-build Run 'go build' with special minification flags
-windows-build Run 'go build' with special Windows support
-android-build Build an Android APK (using the android-qt5 or android-qt6
container environments)
Environment variables:
DOCKER Override the path to docker
Available container environments: (use - as wildcard character)
[...]
```
You can specify the environment using a short form of the name. For example,
`win64-static` will be expanded to the regex `/win64.+static/` and pick the
best available match with the highest version number (`win64-cross-go1.24-qt6.8-static`
at time of writing).
This allows you to pin platforms and major versions in build commands while
automatically upgrading to minor versions.
Example build commands:
```bash
miqt-docker macos go build -ldflags '-s -w'
miqt-docker native -minify-build
miqt-docker win64-qt6-static -windows-build
miqt-docker win64-qt6-static /bin/bash
miqt-docker android-qt6 -android-build
```
## Comparison to manual Docker commands
You can create a dockerized MIQT build environment yourself using the Dockerfiles
in the `docker/` directory. The benefit of miqt-docker is:
- Embeds all available MIQT docker containers
- Use glob matches to automatically pick the highest version container for target
- Automatically build new docker containers or reuse existing, based on content hash of the Dockerfile
- Consistently named docker images across multiple projects using MIQT
- Automatically bind source code volume from current go.mod / go.work / git repository and preserve relative working directory
- Handles bind-mounting the GOCACHE and GOMODCACHE directories for fast rebuilds
- Handles using the proper uid+gid on Linux
- Automatically detect sudo requirement on Linux
- Convenient predefined tasks
- Advanced build support for Android

View File

@ -0,0 +1,415 @@
#!/bin/bash
#
# android-build.sh allows building a MIQT Go application for Android.
# For details, see the top-level README.md file.
set -Eeuo pipefail
# QT_PATH is pre-set in the Qt 5 docker container environment. Includes trailing slash
# QT_ANDROID is pre-set in the Qt 6 docker container environment
QT_PATH=${QT_PATH:-/usr/local/Qt-5.15.13/}
QT_ANDROID=${QT_ANDROID:-$QT_PATH}
export LC_ALL=C.UTF-8
# get_app_name returns the android app's name. This affects the default name
# in deployment-settings.json and the generated lib.so names.
# You can still customise the package name and the package ID in the xml
# files after generation.
get_app_name() {
basename "$(pwd)"
}
get_stub_soname() {
# libRealAppName_arm64-v8a.so
echo "lib$(get_app_name)_arm64-v8a.so"
}
get_go_soname() {
echo "libMiqtGolangApp_arm64-v8a.so"
}
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 returns a comma-separated list of extra libraries to include in
# the apk package
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
}
# generate_template_contents produces a deployment settings JSON file that is
# understood by the androiddeployqt program.
# Available fields are documented in the template file at:
# @ref /usr/local/Qt-5.15.13/mkspecs/features/android/android_deployment_settings.prf
generate_template_contents() {
cat <<EOF
{
"_description": "Generated by miqt/android-mktemplate",
"application-binary": "$(get_app_name)",
"architectures": {
"arm64-v8a" : "aarch64-linux-android"
},
"android-extra-libs": "$(extra_libs)",
"android-min-sdk-version": "23",
"android-target-sdk-version": "$(target_sdk_version)",
"ndk": "/opt/android-sdk/ndk/$(ndk_version)",
"ndk-host": "linux-x86_64",
"qt": "${QT_ANDROID}",
"sdk": "/opt/android-sdk",
"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
}
EOF
}
# pkg_config_dependencies echoes the pkg-config packages that are needed as
# a baseline for the detected Qt version.
# It's not smart enough to discover other Qt packages yet.
pkg_config_dependencies() {
local QT_VERSION=$(detect_miqt_qt_version)
if [[ $QT_VERSION == 'qt5' ]] ; then
echo Qt5Widgets
echo Qt5AndroidExtras
elif [[ $QT_VERSION == 'qt6' ]] ; then
# There is no AndroidExtras in Qt 6
echo Qt6Widgets
fi
}
# 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`.
android_stub_gen() {
local STUBNAME="miqt-stub-$(date +%s).cpp"
cat > $STUBNAME <<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("$(get_go_soname)", 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, "AndroidMain");
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 \
-L${QT_ANDROID}/plugins/platforms -lplugins_platforms_qtforandroid_arm64-v8a \
$(pkg-config --libs $(pkg_config_dependencies)) \
${STUBNAME} \
"-Wl,-soname,$(basename "$(get_stub_soname)")" \
-o "android-build/libs/arm64-v8a/$(get_stub_soname)"
rm "${STUBNAME}"
}
# require_is_main_package verifies that this is the main Go package.
require_is_main_package() {
if ! grep -Fq 'package main' *.go ; then
echo "This doesn't seem to be the main package" >&2
exit 1
fi
}
# patch_app_main sets up the startup go files so that the go program can be
# built either as c-shared for Android or as a normal program for desktop OSes.
patch_app_main() {
# Replace func main() with app_main()
for srcfile in *.go ; do
sed -i -re 's/^func main\(\) \{/func app_main() {/' "$srcfile"
done
# Add shim startup files
cat <<EOF > startup_android.go
//go:build android
// +build android
package main
import "C" // Required for export support
//export AndroidMain
func AndroidMain() {
app_main()
}
func main() {
// Must be empty
}
EOF
cat <<EOF > startup_other.go
//go:build !android
// +build !android
package main
func main() {
app_main()
}
EOF
gofmt -w startup_android.go || true
gofmt -w startup_other.go || true
# Done
}
# unpatch_app_main undoes the transformation from patch_app_main.
unpatch_app_main() {
# Replace func main() with app_main()
for srcfile in *.go ; do
sed -i -re 's/^func app_main\(\) \{/func main() {/' "$srcfile"
done
# Remove extra startup files
rm startup_android.go || true
rm startup_other.go || true
}
# build_app_so compiles the Go app as a c-shared .so.
build_app_so() {
go build \
-buildmode c-shared \
-ldflags "-s -w -extldflags -Wl,-soname,$(get_go_soname)" \
-o "android-build/libs/arm64-v8a/$(get_go_soname)"
}
sdkmanager() {
echo /opt/android-sdk/cmdline-tools/*/bin/sdkmanager
}
# build_apk calls androiddeployqt to package the android-build directory into
# the final apk.
build_apk() {
# Qt 6 androiddeployqt: Understands the QT_ANDROID_KEYSTORE_STORE_PASS in env
# Qt 5 androiddeployqt: Doesn't - any use of `--sign keystore alias` here
# requires stdin prompt but doesn't pass androiddeployqt's stdin through
# to jarsigner subprocess
# Either way, don't sign the app here, rely on separate jarsigner command
# Work around an issue with Qt 6 sporadically failing to detect that
# we have a valid Qt platform plugin
# TODO why does this happen? Is it related to file sort ordering?
# It really does fix itself after multiple attempts (usually less than 5)
# - When it fails: Error: qmlimportscanner not found at /qmlimportscanner
# - When it works: Error: qmlimportscanner not found at libexec/qmlimportscanner
while ! androiddeployqt \
--input ./deployment-settings.json \
--output ./android-build/ \
--release \
2> >(tee /tmp/androiddeployqt.stderr.log >&2) ; do
if grep -Fq 'Make sure the app links to Qt Gui library' /tmp/androiddeployqt.stderr.log ; then
echo "Detected temporary problem with Qt plugin detection. Retrying..."
sleep 1
else
# real error
exit 1
fi
done
local OUTAPK=$(get_app_name).apk
rm "$OUTAPK" || true
# Zipalign
echo "Zipalign..."
/opt/android-sdk/build-tools/*/zipalign \
-p 4 \
./android-build/build/outputs/apk/release/android-build-release-unsigned.apk \
"$OUTAPK"
# Sign
echo "Signing..."
#jarsigner \
# -verbose \
# -sigalg SHA256withRSA -digestalg SHA256 \
# -keystore ./android.keystore \
# "$OUTAPK" \
# "${QT_ANDROID_KEYSTORE_ALIAS}" \
# -storepass:env QT_ANDROID_KEYSTORE_STORE_PASS \
# -keypass:env QT_ANDROID_KEYSTORE_KEY_PASS
/opt/android-sdk/build-tools/*/apksigner \
sign \
--ks ./android.keystore \
--ks-key-alias "${QT_ANDROID_KEYSTORE_ALIAS}" \
--ks-pass env:QT_ANDROID_KEYSTORE_STORE_PASS \
--key-pass env:QT_ANDROID_KEYSTORE_KEY_PASS \
"$OUTAPK"
}
# detect_env_qt_version detects the system's current Qt version.
detect_env_qt_version() {
if qmake --version | fgrep -q 'Qt version 5' ; then
echo "qt5"
return 0
elif qmake --version | fgrep -q 'Qt version 6' ; then
echo "qt6"
return 0
else
echo "Missing Qt tools in PATH" >&2
exit 1
fi
}
# detect_miqt_qt_version echoes either "qt5", "qt6", or exits bash.
detect_miqt_qt_version() {
local IS_QT5=false
if grep -qF '"github.com/mappu/miqt/qt"' *.go ; then
IS_QT5=true
fi
local IS_QT6=false
if grep -qF '"github.com/mappu/miqt/qt6"' *.go ; then
IS_QT6=true
fi
if [[ $IS_QT5 == true && $IS_QT6 == true ]] ; then
echo "Found qt5 and qt6 imports, confused about what to do next" >&2
exit 1
elif [[ $IS_QT5 == true ]] ; then
echo "qt5"
return 0
elif [[ $IS_QT6 == true ]] ; then
echo "qt6"
return 0
else
echo "Found neither qt5 nor qt6 imports. Is this a MIQT Qt app?" >&2
exit 1
fi
}
generate_default_keystore() {
local GENPASS=storepass_$(cat /dev/urandom | head -c64 | md5sum | cut -d' ' -f1)
keytool \
-genkeypair \
-dname "cn=Miqt, ou=Miqt, o=Miqt, c=US" \
-keyalg RSA \
-alias miqt \
-keypass "${GENPASS}" \
-keystore ./android.keystore \
-storepass "${GENPASS}" \
-validity 20000
echo "QT_ANDROID_KEYSTORE_PATH=./android.keystore" > android.keystore.env
echo "QT_ANDROID_KEYSTORE_ALIAS=miqt" >> android.keystore.env
echo "QT_ANDROID_KEYSTORE_STORE_PASS=${GENPASS}" >> android.keystore.env
echo "QT_ANDROID_KEYSTORE_KEY_PASS=${GENPASS}" >> android.keystore.env
}
# main is the entrypoint for android-build.sh.
main() {
if [[ $(detect_env_qt_version) != $(detect_miqt_qt_version) ]] ; then
echo "The system is $(detect_env_qt_version) but the app uses $(detect_miqt_qt_version). Is this the right container?" >&2
exit 1
fi
require_is_main_package
# Rebuild deployment-settings.json
if [[ ! -f deployment-settings.json ]] ; then
echo "Generating deployment-settings.json..."
generate_template_contents > deployment-settings.json
fi
mkdir -p android-build/libs/arm64-v8a
if [[ ! -f android-build/libs/arm64-v8a/$(get_stub_soname) ]] ; then
echo "Generating stub so..."
android_stub_gen
fi
# Rebuild miqt_golang_app.so
echo "Compiling Go app..."
patch_app_main
build_app_so
unpatch_app_main
# Keypair
if [[ ! -f android.keystore || ! -f android.keystore.env ]] ; then
echo "Signing keystore not found, generating a default one..."
generate_default_keystore
fi
# Load keypair credentials into exported env vars
set -o allexport
source android.keystore.env
set +o allexport
# Generate .apk
echo "Packaging APK..."
build_apk
echo "Complete"
}
main "$@"

105
cmd/miqt-docker/docker.go Normal file
View File

@ -0,0 +1,105 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
)
var (
needsSudo bool
)
// dockerImage describes an image available in the docker daemon.
type dockerImage struct {
ID string
Repository string
Tag string
}
// dockerCommand creates an *exec.Cmd for running docker. It respects the global
// `needsSudo` state.
func dockerCommand(args ...string) *exec.Cmd {
docker := os.Getenv("DOCKER")
if docker == "" {
docker = "docker"
}
if needsSudo {
useArgs := make([]string, 0, len(args)+1)
useArgs = append(useArgs, docker)
useArgs = append(useArgs, args...)
return exec.Command(`sudo`, useArgs...)
}
return exec.Command(docker, args...)
}
// dockerListImages lists all the current docker images.
func dockerListImages() ([]dockerImage, error) {
cmd := dockerCommand(`image`, `ls`, `--format`, `{{json . }}`)
cmd.Stderr = os.Stderr // passthrough
buff, err := cmd.Output()
if err != nil {
if !needsSudo {
// Retry with sudo
log.Println("Retrying with sudo...")
needsSudo = true
return dockerListImages()
}
return nil, err
}
var ret []dockerImage
dec := json.NewDecoder(bytes.NewReader(buff))
for {
var entry dockerImage
err = dec.Decode(&entry)
if err != nil {
if errors.Is(err, io.EOF) {
return ret, nil
}
return nil, err // real error
}
ret = append(ret, entry)
}
}
// dockerFindImage searches all the current docker images to find one named as
// the supplied `repository:tag`.
func dockerFindImage(repository, tag string) (*dockerImage, error) {
images, err := dockerListImages()
if err != nil {
return nil, err
}
for _, im := range images {
if im.Repository == repository && im.Tag == tag {
// found it
return &im, nil
}
}
// No match
return nil, os.ErrNotExist
}
// dockerBuild builds the supplied dockerfile and tags it as repository:tag
// as well as repository:latest.
func dockerBuild(dockerfile []byte, repository, tag string) error {
cmd := dockerCommand(`build`, `-t`, repository+`:`+tag, `-t`, repository+`:latest`, `-`)
cmd.Stderr = os.Stderr
cmd.Stdin = bytes.NewReader(dockerfile)
return cmd.Run()
}

View File

@ -0,0 +1,64 @@
package main
import (
"errors"
"fmt"
"path/filepath"
"runtime"
"strings"
)
// highestCommonParent finds the oldest ancestor of a set of paths.
// If there is no common ancestor, returns / on Linux or an error on Windows.
func highestCommonParent(paths []string) (string, error) {
if len(paths) == 0 {
return "", errors.New("no input")
}
parts := strings.Split(paths[0], string(filepath.Separator))
caseSensitive := runtime.GOOS != "windows"
for _, check := range paths {
checkn := strings.Split(check, string(filepath.Separator))
// If this check path is shorter, the common part must also shrink
if len(checkn) < len(parts) {
parts = parts[0:len(checkn)]
}
for i, checkpart := range checkn[0:len(parts)] { // len(parts) is now <= len(checkn) so this is safe
if caseSensitive {
if parts[i] == checkpart {
continue
}
} else {
// case insensitive comparison
if strings.EqualFold(parts[i], checkpart) {
continue
}
}
// Divergence from i: onwards
parts = parts[0:i]
break
}
// Early failure case
if len(parts) == 0 {
break
}
}
isEmpty := len(parts) == 0 || (len(parts) == 1 && parts[0] == "")
if isEmpty {
if runtime.GOOS == "windows" {
return "", fmt.Errorf("Selected paths have no common ancestor: %v", paths)
}
return `/`, nil
}
return strings.Join(parts, string(filepath.Separator)), nil
}

View File

@ -0,0 +1,78 @@
package main
import (
"runtime"
"testing"
)
func TestHighestCommonParent(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("This test uses platform-specific paths")
}
type testCase struct {
input []string
expect string
}
cases := []testCase{
// Single input
testCase{
input: []string{`/foo/bar/baz`},
expect: `/foo/bar/baz`,
},
// Duplicated input
testCase{
input: []string{`/foo/bar/baz`, `/foo/bar/baz`},
expect: `/foo/bar/baz`,
},
// Trailing slashes are preserved if they all included trailing slashes
testCase{
input: []string{`/foo/bar/baz/`, `/foo/bar/baz/`},
expect: `/foo/bar/baz/`,
},
// Common directory
testCase{
input: []string{`/foo/bar/baz`, `/foo/quux`},
expect: `/foo`,
},
// Common directory, multiple inputs
testCase{
input: []string{`/foo/a`, `/foo/b`, `/foo/c`, `/foo/d`},
expect: `/foo`,
},
testCase{
input: []string{`/foo/bar/baz`, `/unrelated`, `/foo/bar/baz`},
expect: `/`,
},
// No leading forwardslash (single input)
testCase{
input: []string{`foo/bar/baz`},
expect: `foo/bar/baz`,
},
// No leading forwardslash (empty output assumes /)
testCase{
input: []string{`foo/bar/baz`, `unrelated`, `foo/bar/baz`},
expect: `/`,
},
}
for idx, tc := range cases {
got, err := highestCommonParent(tc.input)
if err != nil {
t.Errorf("test %d: input(%v) got error=%v", idx, tc.input, err)
continue
}
if got != tc.expect {
t.Errorf("test %d: input(%v) got %q, want %q", idx, tc.input, got, tc.expect)
}
}
}

View File

@ -0,0 +1,24 @@
//+build linux
//go:build linux
package main
import (
"os"
"syscall"
"unsafe"
)
func isatty() bool {
fd := os.Stdout.Fd()
req := syscall.TCGETS
termios := syscall.Termios{}
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(&termios)))
if errno != 0 {
return false
}
// Successfully got Termios info = stdout is a tty
return true
}

View File

@ -0,0 +1,8 @@
//+build !linux
//go:build !linux
package main
func isatty() bool {
return true
}

296
cmd/miqt-docker/main.go Normal file
View File

@ -0,0 +1,296 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/mappu/miqt/docker"
)
// glob2regex converts the glob pattern into a regexp.
// It only supports `-` as a special character meaning 'anything'.
// The resulting regex is unanchored i.e. can match anywhere within a target string.
func glob2regex(pattern string) *regexp.Regexp {
parts := strings.Split(pattern, `-`)
for i, p := range parts {
parts[i] = regexp.QuoteMeta(p)
}
return regexp.MustCompile(strings.Join(parts, `.*`))
}
// shasum returns the hex sha256 of a byte slice.
func shasum(data []byte) string {
hashdata := sha256.Sum256(data)
return hex.EncodeToString(hashdata[:])
}
// usage displays how to use miqt-docker and then exits the process.
func usage(dockerfiles []fs.DirEntry) {
fmt.Fprint(os.Stderr, `Usage: `+filepath.Base(os.Args[0])+` ENVIRONMENT COMMAND...
COMMAND may be any shell command (e.g. go build); or /bin/bash to get an
interactive terminal; or one of the following special tasks:
-build Run 'go build' with usual MIQT flags
-minify-build Run 'go build' with special minification flags
-windows-build Run 'go build' with special Windows support
-android-build Build an Android APK (using the android-qt5 or android-qt6
container environments)
Environment variables:
DOCKER Override the path to docker
Available container environments: (use - as wildcard character)
native (Run natively without docker)
`)
for _, ff := range dockerfiles {
fmt.Fprintf(os.Stderr, " %s\n", strings.TrimSuffix(ff.Name(), `.Dockerfile`))
}
os.Exit(1)
}
// getDockerRunArgsForGlob returns a []string array of all the {busywork} arguments
// for a `docker {run -e -v ...} go build` command.
// It does glob matching for the target container, and builds it if it does not yet exist.
func getDockerRunArgsForGlob(dockerfiles []fs.DirEntry, containerNameGlob string, isatty bool) ([]string, error) {
requestEnvironment := glob2regex(containerNameGlob)
var match string
for _, ff := range dockerfiles {
if !requestEnvironment.MatchString(ff.Name()) {
continue
}
match = ff.Name()
// continue searching for a later match with higher version number
}
if match == "" {
return nil, fmt.Errorf("No available environment matches the request %q\n", containerNameGlob)
}
if !(match == os.Args[1] || match == os.Args[1]+`.Dockerfile`) {
// An inexact/glob match was involved. Show what it was
log.Printf("Selecting dockerfile: %s", match)
}
dockerFileContent, err := docker.ReadFile(match)
if err != nil {
return nil, err // shouldn't happen
}
dockerfileHash := shasum(dockerFileContent)[:8] // First 8 characters of content hash
// Check to see if this dockerfile has already been turned into an image
containerName := `miqt-docker/` + strings.TrimSuffix(match, `.Dockerfile`)
_, err = dockerFindImage(containerName, dockerfileHash)
if err != nil {
if err != os.ErrNotExist {
return nil, err // real error
}
log.Printf("No matching docker image, creating...")
err = dockerBuild(dockerFileContent, containerName, dockerfileHash)
if err != nil {
return nil, err
}
// Search again
_, err = dockerFindImage(containerName, dockerfileHash)
if err != nil {
return nil, fmt.Errorf("Failed to build container for %s:%s: %w", containerName, dockerfileHash, err) // Any error now is a real error
}
}
// Container match found - clean up older containers for the same tag
allContainers, err := dockerListImages()
if err != nil {
return nil, err
}
for _, ctr := range allContainers {
if ctr.Repository == containerName &&
!(ctr.Tag == dockerfileHash || ctr.Tag == "latest") {
log.Printf("Removing previous version container %s:%s ...", containerName, ctr.Tag)
rmCmd := dockerCommand(`image`, `rm`, containerName+`:`+ctr.Tag)
rmCmd.Stdout = os.Stdout
rmCmd.Stderr = os.Stderr
err = rmCmd.Run()
if err != nil {
log.Printf("Warning: Failed to remove previous container: %v", err.Error())
// log and continue
}
}
}
// Container match found - safe to run our command
fullCommand := []string{"run", "--rm", "-i"}
if isatty {
fullCommand = append(fullCommand, "-t")
}
if runtime.GOOS != "windows" {
userinfo, err := user.Current()
if err != nil {
log.Panic(err)
}
fullCommand = append(fullCommand, `--user`, userinfo.Uid+`:`+userinfo.Gid)
}
// Find the GOMODCACHE and GOCACHE to populate mapped volumes
gomodcache, err := exec.Command(`go`, `env`, `GOMODCACHE`).Output()
if err != nil {
return nil, fmt.Errorf("Finding GOMODCACHE: %w", err)
}
if gomodcache_sz := strings.TrimSpace(string(gomodcache)); len(gomodcache_sz) > 0 {
_ = os.MkdirAll(gomodcache_sz, 0755) // Might not exist if no Go modules have been used yet
fullCommand = append(fullCommand, `-v`, gomodcache_sz+`:/go/pkg/mod`, `-e`, `GOMODCACHE=/go/pkg/mod`)
}
gocache, err := exec.Command(`go`, `env`, `GOCACHE`).Output()
if err != nil {
return nil, fmt.Errorf("Finding GOCACHE: %w", err)
}
if gocache_sz := strings.TrimSpace(string(gocache)); len(gocache_sz) > 0 {
_ = os.MkdirAll(gocache_sz, 0755) // Might not exist if no Go packages have been built yet
fullCommand = append(fullCommand, `-v`, gocache_sz+`:/.cache/go-build`, `-e`, `GOCACHE=/.cache/go-build`)
}
// We need to bind-mount probably not just the current working directory,
// but upwards to the root git repo / go.mod file / go.work file (whichever
// is highest)
var parentPaths []string
gomod, err := exec.Command(`go`, `env`, `GOMOD`).Output()
if err != nil {
return nil, fmt.Errorf("Finding GOMOD: %w", err)
}
if gomod_sz := strings.TrimSpace(string(gomod)); len(gomod_sz) > 0 {
parentPaths = append(parentPaths, gomod_sz)
}
gowork, err := exec.Command(`go`, `env`, `GOWORK`).Output()
if err != nil {
return nil, fmt.Errorf("Finding GOWORK: %w", err)
}
if gowork_sz := strings.TrimSpace(string(gowork)); len(gowork_sz) > 0 {
parentPaths = append(parentPaths, gowork_sz)
}
gitroot, err := exec.Command(`git`, `rev-parse`, `--show-toplevel`).Output()
if err != nil {
// Maybe this isn't a git repository? Git is optional anyway, there are hg/bzr users
// Don't panic
} else {
if gitroot_sz := strings.TrimSpace(string(gitroot)); len(gitroot_sz) > 0 {
parentPaths = append(parentPaths, gitroot_sz)
}
}
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
parentPaths = append(parentPaths, cwd) // It's an option too
basedir, err := highestCommonParent(parentPaths)
if err != nil {
return nil, err
}
relCwd, err := filepath.Rel(basedir, cwd)
if err != nil {
return nil, err
}
// Don't mount directly on /src , otherwise -android-build will not know
// the package name for top-level builds. Use a subfolder within it
mountDir := `/src/` + filepath.Base(cwd)
if runtime.GOOS == "windows" {
// convert C:\foo\bar paths to /c/foo/bar that Docker understands
// Otherwise, you experience "invalid mode" when the : is parsed
basedir = `/` + strings.ToLower(string(basedir[0])) + `/` + strings.ReplaceAll(basedir[3:], `\`, `/`)
// Always forwardslashes for in-docker paths, even on Windows OS
mountDir = strings.ReplaceAll(mountDir, `\`, `/`)
}
fullCommand = append(fullCommand,
`-v`, basedir+`:`+mountDir,
`-w`, path.Join(mountDir, relCwd),
// Final standard docker commands
`-e`, `HOME=/tmp`,
containerName+`:`+dockerfileHash,
)
return fullCommand, nil
}
func main() {
dockerfiles, err := docker.Dockerfiles.ReadDir(`.`)
if err != nil {
log.Fatal(err)
}
if len(os.Args) < 3 {
usage(dockerfiles)
}
taskArgs, taskOp, taskAllowTty, err := evaluateTask(os.Args[2:])
if err != nil {
log.Fatal(err)
}
var cmd *exec.Cmd
if os.Args[1] == "native" {
if taskArgs[0] == `/bin/bash` && runtime.GOOS == "windows" {
log.Fatal("This command can't be used in 'native' mode on Windows.")
}
cmd = exec.Command(taskArgs[0], taskArgs[1:]...) // n.b. [1:] may be an empty slice
} else {
dockerArgs, err := getDockerRunArgsForGlob(dockerfiles, os.Args[1], taskAllowTty && isatty())
if err != nil {
log.Fatal(err)
}
dockerArgs = append(dockerArgs, taskArgs...)
cmd = dockerCommand(dockerArgs...)
}
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
taskOp(cmd)
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
}

77
cmd/miqt-docker/tasks.go Normal file
View File

@ -0,0 +1,77 @@
package main
import (
"bytes"
"embed"
"errors"
"fmt"
"os/exec"
)
//go:embed android-build.sh
var embedAndroidBuildSh []byte
var _ embed.FS // Workaround to allow import of package `embed`
// evaluateTask turns the supplied process arguments into real arguments to
// execute, handling quick command recipes as well as arbitrary execution
func evaluateTask(taskArgs []string) (retArgs []string, fixup func(*exec.Cmd), allowTty bool, err error) {
if len(taskArgs) == 0 {
return nil, nil, false, errors.New("No task specified")
}
if len(taskArgs[0]) == 0 {
return nil, nil, false, errors.New("Empty-string first command")
}
// Set up defaults
retArgs = []string{}
fixup = func(*exec.Cmd) {} // no-op
allowTty = true
//
stdinFrom := func(stdinBytes []byte) func(*exec.Cmd) {
return func(c *exec.Cmd) {
c.Stdin = bytes.NewReader(stdinBytes)
}
}
//
if taskArgs[0][0] != '-' {
// Task does not start with a hyphen = plain command
retArgs = taskArgs
return
}
switch taskArgs[0] {
case `-build`:
retArgs = []string{"go", "build", "-ldflags", "-s -w"}
retArgs = append(retArgs, taskArgs[1:]...)
return
case `-windows-build`:
retArgs = []string{"go", "build", "-ldflags", "-s -w -H windowsgui"}
retArgs = append(retArgs, taskArgs[1:]...)
return
case `-minify-build`:
// @ref https://github.com/mappu/miqt/issues/147#issuecomment-2800331135
retArgs = []string{`/bin/bash`, `-c`, "CGO_CFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_CXXFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_LDFLAGS='-Wl,--gc-sections -flto=auto -fwhole-program' go build -ldflags '-s -w'"}
retArgs = append(retArgs, taskArgs[1:]...)
return
case `-android-build`:
retArgs = []string{"/bin/bash", "-s"}
retArgs = append(retArgs, taskArgs[1:]...)
fixup = stdinFrom(embedAndroidBuildSh)
allowTty = false
return
default:
return nil, nil, false, fmt.Errorf("Unrecognized task %q", taskArgs[0])
}
}

View File

@ -8,6 +8,7 @@ To specify the CFLAGS/CXXFLAGS and LDFLAGS for a specific library, make a `MyLib
```pkgconfig ```pkgconfig
Name: My Library Name: My Library
Requires: Qt6Widgets
Libs: -lfoo Libs: -lfoo
Cflags: -I/path/ Cflags: -I/path/
``` ```
@ -16,6 +17,8 @@ Then run `PKG_CONFIG_PATH=/path/to/dir/ go build` so CGO will find your library.
The `PKG_CONFIG_PATH` environment variable is understood both by CGO and by genbindings. The `PKG_CONFIG_PATH` environment variable is understood both by CGO and by genbindings.
When running genbindings in the docker/genbindings container, custom pkg-config files are created inline in the Dockerfile.
## Further reading ## Further reading
- [Guide to pkg-config](https://people.freedesktop.org/~dbn/pkg-config-guide.html) - [Guide to pkg-config](https://people.freedesktop.org/~dbn/pkg-config-guide.html)
@ -25,3 +28,23 @@ The `PKG_CONFIG_PATH` environment variable is understood both by CGO and by genb
$ pkg-config --variable pc_path pkg-config $ pkg-config --variable pc_path pkg-config
/usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig /usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
``` ```
List all available packages known to pkg-config:
```bash
pkg-config --list-all
```
A full example for QScintilla (Qt 5) from the [genbindings.Dockerfile](../docker/genbindings.Dockerfile):
```pkg-config
includedir=/usr/include/x86_64-linux-gnu/qt5/Qsci/
Name: QScintilla
Description: Qt5 port of the Scintilla source code editing widget
URL: http://www.riverbankcomputing.co.uk/software/qscintilla
Version: 2.13.3
Requires: Qt5Widgets, Qt5PrintSupport
Libs: -lqscintilla2_qt5
Cflags: -I${includedir}
```

View File

@ -4,9 +4,6 @@ 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 && \ tar x -C /usr/local/ -f go1.23.1.linux-amd64.tar.gz && \
rm 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-mktemplate/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 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
# The pkg-config definitions were all installed with platform-specific suffixes # The pkg-config definitions were all installed with platform-specific suffixes
@ -14,6 +11,10 @@ ENV PATH=/usr/local/go/bin:/opt/cmake/bin:/usr/local/sbin:/usr/local/bin:/usr/sb
# This container is targeting armv8-a, so set up simple symlinks # This container is targeting armv8-a, so set up simple symlinks
RUN /bin/bash -c 'cd /usr/local/Qt-5.15.13/lib/pkgconfig ; for f in *_arm64-v8a.pc ; do cp $f "$(basename -s _arm64-v8a.pc "$f").pc"; done' RUN /bin/bash -c 'cd /usr/local/Qt-5.15.13/lib/pkgconfig ; for f in *_arm64-v8a.pc ; do cp $f "$(basename -s _arm64-v8a.pc "$f").pc"; done'
# This is gross but (A) it's containerized and (B) allows --uid=1000 to perform builds
# Only needed for certain problematic versions of the Android SDK; a readonly SDK works in both older+newer SDKs
RUN /bin/bash -c 'find /opt/android-sdk/ -type d -exec chmod 777 {} \; && find /opt/android-sdk/ -perm 660 -exec chmod 666 {} \;'
ENV CC=/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang 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 CXX=/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1

View File

@ -9,9 +9,6 @@ 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 && \ tar x -C /usr/local/ -f go1.23.6.linux-amd64.tar.gz && \
rm 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: # 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 # 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 # 2. It looks for `Libs: -L${libdir} -lQt6Widgets` but the file is named libQt6Widgets_arm64-v8a.so

16
docker/embed.go Normal file
View File

@ -0,0 +1,16 @@
// This Go file exports all the *.Dockerfile files for miqt-docker to use.
package docker
import (
"embed"
)
//go:embed *.Dockerfile
var Dockerfiles embed.FS
// ReadFile returns the content of one of the dockerfiles.
// That's because an embed.FS appears out-of-package as a []fs.DirEntry, which
// isn't directly readable.
func ReadFile(name string) ([]byte, error) {
return Dockerfiles.ReadFile(name)
}

View File

@ -39,10 +39,43 @@ RUN \
qmake && \ qmake && \
make make
# Custom pkg-config definitions
RUN mkdir -p /usr/local/lib/pkgconfig RUN mkdir -p /usr/local/lib/pkgconfig
COPY pkg-config/QScintilla.pc.example /usr/local/lib/pkgconfig/QScintilla.pc RUN echo 'includedir=/usr/include/x86_64-linux-gnu/qt5/Qsci/' \
COPY pkg-config/QScintilla6.pc.example /usr/local/lib/pkgconfig/QScintilla6.pc '\n' \
COPY pkg-config/ScintillaEdit.pc.example /usr/local/lib/pkgconfig/ScintillaEdit.pc '\nName: QScintilla' \
'\nDescription: Qt5 port of the Scintilla source code editing widget' \
'\nURL: http://www.riverbankcomputing.co.uk/software/qscintilla' \
'\nVersion: 2.13.3' \
'\nRequires: Qt5Widgets, Qt5PrintSupport' \
'\nLibs: -lqscintilla2_qt5' \
'\nCflags: -I${includedir}' \
> /usr/local/lib/pkgconfig/QScintilla.pc
RUN echo 'includedir=/usr/include/x86_64-linux-gnu/qt6/Qsci/' \
'\n' \
'\nName: QScintilla6' \
'\nDescription: Qt6 port of the Scintilla source code editing widget' \
'\nURL: http://www.riverbankcomputing.co.uk/software/qscintilla' \
'\nVersion: 2.13.3' \
'\nRequires: Qt6Widgets, Qt6PrintSupport' \
'\nLibs: -lqscintilla2_qt6' \
'\nCflags: -I${includedir}' \
> /usr/local/lib/pkgconfig/QScintilla6.pc
RUN echo 'srcdir=/usr/local/src/scintilla/' \
'\n' \
'\nName: ScintillaEdit' \
'\nDescription: Scintilla upstream Qt port' \
'\nURL: https://www.scintilla.org/' \
'\nVersion: 5.5.2' \
'\nRequires: Qt5Widgets' \
'\nLibs: -L${srcdir}/bin -lScintillaEdit' \
'\nCflags: -include stdint.h -I${srcdir}/qt/ScintillaEdit -I${srcdir}/qt/ScintillaEditBase -I${srcdir}/include -I${srcdir}/src' \
> /usr/local/lib/pkgconfig/ScintillaEdit.pc
#
ENV GOFLAGS=-buildvcs=false ENV GOFLAGS=-buildvcs=false

View File

@ -1,29 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/mappu/miqt/qt"
)
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!")
}

View File

@ -1,14 +0,0 @@
// +build android
package main
import "C" // Required for export support
//export AndroidMain
func AndroidMain() {
myRealMainFunc()
}
func main() {
// Must be empty
}

View File

@ -1,7 +0,0 @@
// +build !android
package main
func main() {
myRealMainFunc()
}

View File

@ -1,29 +0,0 @@
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!")
}

View File

@ -1,14 +0,0 @@
// +build android
package main
import "C" // Required for export support
//export AndroidMain
func AndroidMain() {
myRealMainFunc()
}
func main() {
// Must be empty
}

View File

@ -1,7 +0,0 @@
// +build !android
package main
func main() {
myRealMainFunc()
}

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,9 +0,0 @@
includedir=/usr/include/x86_64-linux-gnu/qt5/Qsci/
Name: QScintilla
Description: Qt5 port of the Scintilla source code editing widget
URL: http://www.riverbankcomputing.co.uk/software/qscintilla
Version: 2.13.3
Requires: Qt5Widgets, Qt5PrintSupport
Libs: -lqscintilla2_qt5
Cflags: -I${includedir}

View File

@ -1,9 +0,0 @@
includedir=/usr/include/x86_64-linux-gnu/qt6/Qsci/
Name: QScintilla6
Description: Qt6 port of the Scintilla source code editing widget
URL: http://www.riverbankcomputing.co.uk/software/qscintilla
Version: 2.13.3
Requires: Qt6Widgets, Qt6PrintSupport
Libs: -lqscintilla2_qt6
Cflags: -I${includedir}

View File

@ -1,9 +0,0 @@
srcdir=/usr/local/src/scintilla/
Name: ScintillaEdit
Description: Scintilla's own upstream Qt port
URL: https://www.scintilla.org/
Version: 5.5.2
Requires: Qt5Widgets
Libs: -L${srcdir}/bin -lScintillaEdit
Cflags: -include stdint.h -I${srcdir}/qt/ScintillaEdit -I${srcdir}/qt/ScintillaEditBase -I${srcdir}/include -I${srcdir}/src