From 540b3067152037e226e9fe553e79255015b48b07 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 12:06:46 +1200 Subject: [PATCH 01/26] miqt-docker: initial commit --- .gitignore | 1 + cmd/miqt-docker/README.md | 33 +++++ cmd/miqt-docker/docker.go | 105 ++++++++++++++++ cmd/miqt-docker/filepath.go | 54 ++++++++ cmd/miqt-docker/filepath_test.go | 78 ++++++++++++ cmd/miqt-docker/main.go | 208 +++++++++++++++++++++++++++++++ docker/embed.go | 16 +++ 7 files changed, 495 insertions(+) create mode 100644 cmd/miqt-docker/README.md create mode 100644 cmd/miqt-docker/docker.go create mode 100644 cmd/miqt-docker/filepath.go create mode 100644 cmd/miqt-docker/filepath_test.go create mode 100644 cmd/miqt-docker/main.go create mode 100644 docker/embed.go diff --git a/.gitignore b/.gitignore index 6e2e35e0..88a4ebbb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ cmd/handbindings/handbindings cmd/handbindings/bindings_test/direct cmd/handbindings/bindings_test/testapp cmd/genbindings/genbindings +cmd/miqt-docker/miqt-docker cmd/miqt-uic/miqt-uic cmd/miqt-rcc/miqt-rcc diff --git a/cmd/miqt-docker/README.md b/cmd/miqt-docker/README.md new file mode 100644 index 00000000..be5b9b02 --- /dev/null +++ b/cmd/miqt-docker/README.md @@ -0,0 +1,33 @@ +# miqt-docker + +This is a helper program to quickly run a dockerized MIQT build environment. + +- Supports 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 +- 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 +- Handles using the proper uid+gid on Linux +- Automatically detect sudo requirement on Linux + +## Usage + +Run `miqt-docker` with no arguments to see full usage instructions and all +available embedded dockerfiles: + +```bash +Usage: miqt-docker ENVIRONMENT COMMAND... + +Environment variables: +- DOCKER Override the path to docker + +Available container environments (use * for partial match): +[...] +``` + +Example build commands: + +```bash +miqt-docker macos go build -ldflags '-s -w' +miqt-docker win64*qt6*dynamic go build -ldflags '-s -w -H windowsgui' +``` diff --git a/cmd/miqt-docker/docker.go b/cmd/miqt-docker/docker.go new file mode 100644 index 00000000..77cf728c --- /dev/null +++ b/cmd/miqt-docker/docker.go @@ -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() +} diff --git a/cmd/miqt-docker/filepath.go b/cmd/miqt-docker/filepath.go new file mode 100644 index 00000000..5a5661c8 --- /dev/null +++ b/cmd/miqt-docker/filepath.go @@ -0,0 +1,54 @@ +package main + +import ( + "errors" + "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)) + + 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 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 "", errors.New("Selected paths have no common ancestor") + } + return `/`, nil + } + + return strings.Join(parts, string(filepath.Separator)), nil +} diff --git a/cmd/miqt-docker/filepath_test.go b/cmd/miqt-docker/filepath_test.go new file mode 100644 index 00000000..2888e768 --- /dev/null +++ b/cmd/miqt-docker/filepath_test.go @@ -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) + } + } +} diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go new file mode 100644 index 00000000..6a8cb69f --- /dev/null +++ b/cmd/miqt-docker/main.go @@ -0,0 +1,208 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io/fs" + "log" + "os" + "os/exec" + "os/user" + "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. +// 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.Fprintf(os.Stderr, "Usage: %s ENVIRONMENT COMMAND...\n", filepath.Base(os.Args[0])) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Environment variables:") + fmt.Fprintln(os.Stderr, "- DOCKER Override the path to docker") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Available container environments (use * for partial match):") + + for _, ff := range dockerfiles { + fmt.Fprintf(os.Stderr, "- %s\n", strings.TrimSuffix(ff.Name(), `.Dockerfile`)) + } + + os.Exit(1) +} + +func main() { + + dockerfiles, err := docker.Dockerfiles.ReadDir(`.`) + if err != nil { + log.Panic(err) + } + + if len(os.Args) < 3 { + usage(dockerfiles) + } + + requestEnvironment := glob2regex(os.Args[1]) + 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 == "" { + log.Fatalf("No available environment matches the request %q\n", os.Args[1]) + } + + 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 { + log.Panic(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 { + log.Panic(err) // real error + } + + log.Printf("No matching docker image, creating...") + err = dockerBuild(dockerFileContent, containerName, dockerfileHash) + if err != nil { + log.Panic(err) + } + + // Search again + _, err = dockerFindImage(containerName, dockerfileHash) + if err != nil { + log.Printf("Failed to build container for %s:%s", containerName, dockerfileHash) + log.Panic(err) // Any error now is a real error + } + } + + // Container match found - safe to run our command + + fullCommand := []string{"run"} + + 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 { + log.Panic(err) + } + if gomodcache_sz := strings.TrimSpace(string(gomodcache)); len(gomodcache_sz) > 0 { + 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 { + log.Panic(err) + } + if gocache_sz := strings.TrimSpace(string(gocache)); len(gocache_sz) > 0 { + 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 { + log.Panic(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 { + log.Panic(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 { + log.Panic(err) + } + + parentPaths = append(parentPaths, cwd) // It's an option too + + basedir, err := highestCommonParent(parentPaths) + if err != nil { + log.Panic(err) + } + + relCwd, err := filepath.Rel(basedir, cwd) + if err != nil { + log.Panic(err) + } + + fullCommand = append(fullCommand, `-v`, basedir+`:/src`, `-w`, filepath.Join(`/src`, relCwd)) + + // Final standard docker commands + + fullCommand = append(fullCommand, containerName+`:`+dockerfileHash) // , `/bin/bash`, `-c`) + fullCommand = append(fullCommand, os.Args[2:]...) + + cmd := dockerCommand(fullCommand...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err = cmd.Run() + if err != nil { + log.Fatal(err) + } +} diff --git a/docker/embed.go b/docker/embed.go new file mode 100644 index 00000000..21c019c4 --- /dev/null +++ b/docker/embed.go @@ -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) +} From e7892c780e80d8d8ebbe852b53f3527e594e1d6d Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:22:53 +1200 Subject: [PATCH 02/26] miqt-docker: create local GOCACHE/GOMODCACHE if they do not yet exist --- cmd/miqt-docker/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index 6a8cb69f..fde85258 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -132,6 +132,8 @@ func main() { log.Panic(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`) } @@ -140,6 +142,8 @@ func main() { log.Panic(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`) } From 7dd11c6d9b14f815129e236b769443c7c52d0562 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:23:27 +1200 Subject: [PATCH 03/26] docker/genbindings: inline pkg-config defintions for context-free builds --- Makefile | 2 +- docker/genbindings.Dockerfile | 39 ++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index b28558ad..f0fe9c1e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ DOCKEREXEC = mkdir -p "$$(go env GOCACHE)" && \ all: genbindings docker/genbindings.docker-buildstamp: docker/genbindings.Dockerfile - $(DOCKER) build -t miqt/genbindings:latest -f docker/genbindings.Dockerfile . + $(DOCKER) build -t miqt/genbindings:latest - < docker/genbindings.Dockerfile touch $@ .PHONY: clean diff --git a/docker/genbindings.Dockerfile b/docker/genbindings.Dockerfile index 73f8850b..08fffe54 100644 --- a/docker/genbindings.Dockerfile +++ b/docker/genbindings.Dockerfile @@ -39,10 +39,43 @@ RUN \ qmake && \ make +# Custom pkg-config definitions + RUN mkdir -p /usr/local/lib/pkgconfig -COPY pkg-config/QScintilla.pc.example /usr/local/lib/pkgconfig/QScintilla.pc -COPY pkg-config/QScintilla6.pc.example /usr/local/lib/pkgconfig/QScintilla6.pc -COPY pkg-config/ScintillaEdit.pc.example /usr/local/lib/pkgconfig/ScintillaEdit.pc +RUN echo 'includedir=/usr/include/x86_64-linux-gnu/qt5/Qsci/' \ + '\n' \ + '\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 From c7b7e8fdfbc633d0beee7461cc2d7311c2cabcf6 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:32:57 +1200 Subject: [PATCH 04/26] pkg-config: move into docs/, delete the example files --- .gitignore | 3 --- README.md | 2 +- cmd/genbindings/README.md | 2 +- pkg-config/README.md => doc/pkg-config.md | 23 +++++++++++++++++++++++ pkg-config/QScintilla.pc.example | 9 --------- pkg-config/QScintilla6.pc.example | 9 --------- pkg-config/ScintillaEdit.pc.example | 9 --------- 7 files changed, 25 insertions(+), 32 deletions(-) rename pkg-config/README.md => doc/pkg-config.md (67%) delete mode 100644 pkg-config/QScintilla.pc.example delete mode 100644 pkg-config/QScintilla6.pc.example delete mode 100644 pkg-config/ScintillaEdit.pc.example diff --git a/.gitignore b/.gitignore index 88a4ebbb..78b7af9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ container-build-cache/ # local genbindings configuration cmd/genbindings/genbindings.local* -# local pkg-config configuration -pkg-config/*.pc - # binaries *.exe diff --git a/README.md b/README.md index 88d35c91..1132e8c3 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ 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. -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? diff --git a/cmd/genbindings/README.md b/cmd/genbindings/README.md index 5ea63b63..d7c6adc3 100644 --- a/cmd/genbindings/README.md +++ b/cmd/genbindings/README.md @@ -38,7 +38,7 @@ You should check the following configuration: 1. Git clone this repository 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 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 diff --git a/pkg-config/README.md b/doc/pkg-config.md similarity index 67% rename from pkg-config/README.md rename to doc/pkg-config.md index e26e0317..3e69f662 100644 --- a/pkg-config/README.md +++ b/doc/pkg-config.md @@ -8,6 +8,7 @@ To specify the CFLAGS/CXXFLAGS and LDFLAGS for a specific library, make a `MyLib ```pkgconfig Name: My Library +Requires: Qt6Widgets Libs: -lfoo 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. +When running genbindings in the docker/genbindings container, custom pkg-config files are created inline in the Dockerfile. + ## Further reading - [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 /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} +``` diff --git a/pkg-config/QScintilla.pc.example b/pkg-config/QScintilla.pc.example deleted file mode 100644 index fc740710..00000000 --- a/pkg-config/QScintilla.pc.example +++ /dev/null @@ -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} diff --git a/pkg-config/QScintilla6.pc.example b/pkg-config/QScintilla6.pc.example deleted file mode 100644 index fd6f6af5..00000000 --- a/pkg-config/QScintilla6.pc.example +++ /dev/null @@ -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} diff --git a/pkg-config/ScintillaEdit.pc.example b/pkg-config/ScintillaEdit.pc.example deleted file mode 100644 index 9146b64b..00000000 --- a/pkg-config/ScintillaEdit.pc.example +++ /dev/null @@ -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 From 8328ca7e1235978c0f855c52d49a645b5e817d95 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:33:11 +1200 Subject: [PATCH 05/26] gitignore: remove some obsolete local excludes --- .gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitignore b/.gitignore index 78b7af9e..424691d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ # cache files for genbindings cmd/genbindings/cachedir/ -# docker files -container-build-cache/ - -# local genbindings configuration -cmd/genbindings/genbindings.local* - # binaries *.exe From 69190ece2c981a1bcaf89c3b1b8dd2c670e308fe Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:47:34 +1200 Subject: [PATCH 06/26] miqt-docker: use -it to ensure ^C signals make it through --- cmd/miqt-docker/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index fde85258..87fc5dd8 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -115,7 +115,7 @@ func main() { // Container match found - safe to run our command - fullCommand := []string{"run"} + fullCommand := []string{"run", "-it"} if runtime.GOOS != "windows" { userinfo, err := user.Current() @@ -203,6 +203,7 @@ func main() { fullCommand = append(fullCommand, os.Args[2:]...) cmd := dockerCommand(fullCommand...) + cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout err = cmd.Run() From b3d5f541f06b74a0500f8a7f8230412dcba16d09 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 18:39:38 +1200 Subject: [PATCH 07/26] miqt-docker: conditionally use -it if parent was a tty --- cmd/miqt-docker/isatty_linux.go | 24 ++++++++++++++++++++++++ cmd/miqt-docker/isatty_other.go | 8 ++++++++ cmd/miqt-docker/main.go | 6 +++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 cmd/miqt-docker/isatty_linux.go create mode 100644 cmd/miqt-docker/isatty_other.go diff --git a/cmd/miqt-docker/isatty_linux.go b/cmd/miqt-docker/isatty_linux.go new file mode 100644 index 00000000..920fd9b9 --- /dev/null +++ b/cmd/miqt-docker/isatty_linux.go @@ -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 +} diff --git a/cmd/miqt-docker/isatty_other.go b/cmd/miqt-docker/isatty_other.go new file mode 100644 index 00000000..cd73eb93 --- /dev/null +++ b/cmd/miqt-docker/isatty_other.go @@ -0,0 +1,8 @@ +//+build !linux +//go:build !linux + +package main + +func isatty() bool { + return true +} diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index 87fc5dd8..dd8ffd75 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -115,7 +115,11 @@ func main() { // Container match found - safe to run our command - fullCommand := []string{"run", "-it"} + fullCommand := []string{"run"} + + if isatty() { + fullCommand = append(fullCommand, "-it") + } if runtime.GOOS != "windows" { userinfo, err := user.Current() From ded180f77bb61e4671eca5d0c5fbf6e83d9ae475 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 26 Apr 2025 17:47:41 +1200 Subject: [PATCH 08/26] Makefile: simplify to use miqt-docker --- Makefile | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index f0fe9c1e..3e43d0cd 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,23 @@ -BUILDSTAMPS := docker/genbindings.docker-buildstamp -DOCKER := docker SHELL := /bin/bash - -# 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 +GO := go .PHONY: all all: genbindings -docker/genbindings.docker-buildstamp: docker/genbindings.Dockerfile - $(DOCKER) build -t miqt/genbindings:latest - < docker/genbindings.Dockerfile - touch $@ - -.PHONY: clean -clean: - $(DOCKER) image rm -f miqt/genbindings:latest - rm -f $(BUILDSTAMPS) +cmd/miqt-docker/miqt-docker: go.mod cmd/miqt-docker/*.go docker/*.Dockerfile + $(GO) build -o cmd/miqt-docker/miqt-docker ./cmd/miqt-docker .PHONY: clean-cache clean-cache: rm -f cmd/genbindings/cachedir/*.json +cmd/genbindings/genbindings: go.mod cmd/genbindings/*.go + $(GO) build -o cmd/genbindings/genbindings ./cmd/genbindings + .PHONY: genbindings -genbindings: $(BUILDSTAMPS) - $(DOCKEREXEC) 'cd cmd/genbindings && go build && ./genbindings' +genbindings: cmd/miqt-docker/miqt-docker cmd/genbindings/genbindings + cd cmd/genbindings && ../miqt-docker/miqt-docker genbindings ./genbindings .PHONY: build-all -build-all: $(BUILDSTAMPS) - $(DOCKEREXEC) 'go build ./...' +build-all: cmd/miqt-docker/miqt-docker + ./cmd/miqt-docker/miqt-docker genbindings go build ./... From d075984e77d6ceeb1ee69cecd42c488aefc691d9 Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:42:48 +1200 Subject: [PATCH 09/26] miqt-docker: auto cleanup old container versions --- cmd/miqt-docker/main.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index dd8ffd75..c5cf4d30 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -113,6 +113,27 @@ func main() { } } + // 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"} From b2536be8d5d972389932f997ed14e0a7cf47964c Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:43:13 +1200 Subject: [PATCH 10/26] miqt-docker: change wildcard from * to - to avoid shell interpretation --- cmd/miqt-docker/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index c5cf4d30..a94cea56 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -18,10 +18,10 @@ import ( ) // glob2regex converts the glob pattern into a regexp. -// It only supports `*` as a special character. +// 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, `*`) + parts := strings.Split(pattern, `-`) for i, p := range parts { parts[i] = regexp.QuoteMeta(p) } From c6ff8e05336d54f9794dbf9e7ba4322b4b53793d Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:46:16 +1200 Subject: [PATCH 11/26] miqt-docker: support 'native', add -build task, conditional isatty --- cmd/miqt-docker/main.go | 88 +++++++++++++++++++++++++++------------- cmd/miqt-docker/tasks.go | 44 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 cmd/miqt-docker/tasks.go diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index a94cea56..6d6faffb 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -51,18 +51,12 @@ func usage(dockerfiles []fs.DirEntry) { os.Exit(1) } -func main() { +// 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) { - dockerfiles, err := docker.Dockerfiles.ReadDir(`.`) - if err != nil { - log.Panic(err) - } - - if len(os.Args) < 3 { - usage(dockerfiles) - } - - requestEnvironment := glob2regex(os.Args[1]) + requestEnvironment := glob2regex(containerNameGlob) var match string for _, ff := range dockerfiles { if !requestEnvironment.MatchString(ff.Name()) { @@ -74,7 +68,7 @@ func main() { } if match == "" { - log.Fatalf("No available environment matches the request %q\n", os.Args[1]) + return nil, fmt.Errorf("No available environment matches the request %q\n", containerNameGlob) } if !(match == os.Args[1] || match == os.Args[1]+`.Dockerfile`) { @@ -84,7 +78,7 @@ func main() { dockerFileContent, err := docker.ReadFile(match) if err != nil { - log.Panic(err) // shouldn't happen + return nil, err // shouldn't happen } dockerfileHash := shasum(dockerFileContent)[:8] // First 8 characters of content hash @@ -96,20 +90,19 @@ func main() { _, err = dockerFindImage(containerName, dockerfileHash) if err != nil { if err != os.ErrNotExist { - log.Panic(err) // real error + return nil, err // real error } log.Printf("No matching docker image, creating...") err = dockerBuild(dockerFileContent, containerName, dockerfileHash) if err != nil { - log.Panic(err) + return nil, err } // Search again _, err = dockerFindImage(containerName, dockerfileHash) if err != nil { - log.Printf("Failed to build container for %s:%s", containerName, dockerfileHash) - log.Panic(err) // Any error now is a real error + return nil, fmt.Errorf("Failed to build container for %s:%s: %w", containerName, dockerfileHash, err) // Any error now is a real error } } @@ -136,10 +129,10 @@ func main() { // Container match found - safe to run our command - fullCommand := []string{"run"} + fullCommand := []string{"run", "--rm", "-i"} - if isatty() { - fullCommand = append(fullCommand, "-it") + if isatty { + fullCommand = append(fullCommand, "-t") } if runtime.GOOS != "windows" { @@ -154,7 +147,7 @@ func main() { // Find the GOMODCACHE and GOCACHE to populate mapped volumes gomodcache, err := exec.Command(`go`, `env`, `GOMODCACHE`).Output() if err != nil { - log.Panic(err) + 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 @@ -164,7 +157,7 @@ func main() { gocache, err := exec.Command(`go`, `env`, `GOCACHE`).Output() if err != nil { - log.Panic(err) + 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 @@ -179,7 +172,7 @@ func main() { var parentPaths []string gomod, err := exec.Command(`go`, `env`, `GOMOD`).Output() if err != nil { - log.Panic(err) + return nil, fmt.Errorf("Finding GOMOD: %w", err) } if gomod_sz := strings.TrimSpace(string(gomod)); len(gomod_sz) > 0 { parentPaths = append(parentPaths, gomod_sz) @@ -187,7 +180,7 @@ func main() { gowork, err := exec.Command(`go`, `env`, `GOWORK`).Output() if err != nil { - log.Panic(err) + return nil, fmt.Errorf("Finding GOWORK: %w", err) } if gowork_sz := strings.TrimSpace(string(gowork)); len(gowork_sz) > 0 { parentPaths = append(parentPaths, gowork_sz) @@ -205,19 +198,19 @@ func main() { cwd, err := os.Getwd() if err != nil { - log.Panic(err) + return nil, err } parentPaths = append(parentPaths, cwd) // It's an option too basedir, err := highestCommonParent(parentPaths) if err != nil { - log.Panic(err) + return nil, err } relCwd, err := filepath.Rel(basedir, cwd) if err != nil { - log.Panic(err) + return nil, err } fullCommand = append(fullCommand, `-v`, basedir+`:/src`, `-w`, filepath.Join(`/src`, relCwd)) @@ -225,12 +218,49 @@ func main() { // Final standard docker commands fullCommand = append(fullCommand, containerName+`:`+dockerfileHash) // , `/bin/bash`, `-c`) - fullCommand = append(fullCommand, os.Args[2:]...) - cmd := dockerCommand(fullCommand...) + 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) diff --git a/cmd/miqt-docker/tasks.go b/cmd/miqt-docker/tasks.go new file mode 100644 index 00000000..7509c307 --- /dev/null +++ b/cmd/miqt-docker/tasks.go @@ -0,0 +1,44 @@ +package main + +import ( + "errors" + "fmt" + "os/exec" +) + +// 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 + + // + + 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 + + default: + return nil, nil, false, fmt.Errorf("Unrecognized task %q", taskArgs[0]) + } +} From 34e7429fe197fa03c149d46fe497ee10504c0bc4 Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:46:36 +1200 Subject: [PATCH 12/26] miqt-docker: add -windows-build task --- cmd/miqt-docker/tasks.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/miqt-docker/tasks.go b/cmd/miqt-docker/tasks.go index 7509c307..980dfa2a 100644 --- a/cmd/miqt-docker/tasks.go +++ b/cmd/miqt-docker/tasks.go @@ -38,6 +38,11 @@ func evaluateTask(taskArgs []string) (retArgs []string, fixup func(*exec.Cmd), a retArgs = append(retArgs, taskArgs[1:]...) return + case `-windows-build`: + retArgs = []string{"go", "build", "-ldflags", "-s -w -H windowsgui"} + retArgs = append(retArgs, taskArgs[1:]...) + return + default: return nil, nil, false, fmt.Errorf("Unrecognized task %q", taskArgs[0]) } From 6984b83aeaa55aaa2cab7b9f3a6aa73246809108 Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:46:50 +1200 Subject: [PATCH 13/26] miqt-docker: add -minify-build task --- cmd/miqt-docker/tasks.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/miqt-docker/tasks.go b/cmd/miqt-docker/tasks.go index 980dfa2a..260bba4b 100644 --- a/cmd/miqt-docker/tasks.go +++ b/cmd/miqt-docker/tasks.go @@ -43,6 +43,12 @@ func evaluateTask(taskArgs []string) (retArgs []string, fixup func(*exec.Cmd), a 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 + default: return nil, nil, false, fmt.Errorf("Unrecognized task %q", taskArgs[0]) } From 687579d76f7d837913c406956a8a7d81fc0a8663 Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:47:21 +1200 Subject: [PATCH 14/26] miqt-docker: set $HOME to /tmp inside container --- cmd/miqt-docker/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index 6d6faffb..044d56f0 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -215,6 +215,8 @@ func getDockerRunArgsForGlob(dockerfiles []fs.DirEntry, containerNameGlob string fullCommand = append(fullCommand, `-v`, basedir+`:/src`, `-w`, filepath.Join(`/src`, relCwd)) + fullCommand = append(fullCommand, `-e`, `HOME=/tmp`) + // Final standard docker commands fullCommand = append(fullCommand, containerName+`:`+dockerfileHash) // , `/bin/bash`, `-c`) From a4a4074948874ea954dcb62c2d018234f5f71f9f Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:48:09 +1200 Subject: [PATCH 15/26] android: major rework for all-in-one android support --- cmd/android-mktemplate/android-mktemplate.sh | 66 --- cmd/android-stub-gen/android-stub-gen.sh | 106 ----- cmd/miqt-docker/android-build.sh | 415 ++++++++++++++++++ cmd/miqt-docker/main.go | 4 +- cmd/miqt-docker/tasks.go | 22 + ...id-armv8a-go1.23-qt5.15-dynamic.Dockerfile | 7 +- ...oid-armv8a-go1.23-qt6.6-dynamic.Dockerfile | 3 - 7 files changed, 444 insertions(+), 179 deletions(-) delete mode 100755 cmd/android-mktemplate/android-mktemplate.sh delete mode 100755 cmd/android-stub-gen/android-stub-gen.sh create mode 100644 cmd/miqt-docker/android-build.sh diff --git a/cmd/android-mktemplate/android-mktemplate.sh b/cmd/android-mktemplate/android-mktemplate.sh deleted file mode 100755 index 8ccc3f0a..00000000 --- a/cmd/android-mktemplate/android-mktemplate.sh +++ /dev/null @@ -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}" <&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 < -#include -#include - -typedef void goMainFunc_t(); - -int main(int argc, char** argv) { - __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "Starting up"); - - void* handle = dlopen("$(basename "$ARG_SOURCE_SOFILE")", RTLD_LAZY); - if (handle == NULL) { - __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: null handle opening so: %s", dlerror()); - exit(1); - } - - void* goMain = dlsym(handle, "${ARG_FUNCTIONNAME}"); - if (goMain == NULL) { - __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: null handle looking for function: %s", dlerror()); - exit(1); - } - - __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: Found target, calling"); - - // Cast to function pointer and call - goMainFunc_t* f = (goMainFunc_t*)goMain; - f(); - - __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "miqt_stub: Target function returned"); - return 0; -} - -EOF - - # Compile - # Link with Qt libraries so that androiddeployqt detects us as being the - # main shared library - - 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 "$@" diff --git a/cmd/miqt-docker/android-build.sh b/cmd/miqt-docker/android-build.sh new file mode 100644 index 00000000..7d613f50 --- /dev/null +++ b/cmd/miqt-docker/android-build.sh @@ -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 < $STUBNAME < +#include +#include + +typedef void goMainFunc_t(); + +int main(int argc, char** argv) { + __android_log_print(ANDROID_LOG_VERBOSE, "miqt_stub", "Starting up"); + + void* handle = dlopen("$(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 < 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 < 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 "$@" diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index 044d56f0..ea367fce 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -213,7 +213,9 @@ func getDockerRunArgsForGlob(dockerfiles []fs.DirEntry, containerNameGlob string return nil, err } - fullCommand = append(fullCommand, `-v`, basedir+`:/src`, `-w`, filepath.Join(`/src`, relCwd)) + mountDir := `/src/` + filepath.Base(cwd) // Don't use /src directly, otherwise -android-build will not know the package name for top-level builds + + fullCommand = append(fullCommand, `-v`, basedir+`:`+mountDir, `-w`, filepath.Join(mountDir, relCwd)) fullCommand = append(fullCommand, `-e`, `HOME=/tmp`) diff --git a/cmd/miqt-docker/tasks.go b/cmd/miqt-docker/tasks.go index 260bba4b..903f3668 100644 --- a/cmd/miqt-docker/tasks.go +++ b/cmd/miqt-docker/tasks.go @@ -1,11 +1,18 @@ 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) { @@ -26,6 +33,14 @@ func evaluateTask(taskArgs []string) (retArgs []string, fixup func(*exec.Cmd), a // + 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 @@ -49,6 +64,13 @@ func evaluateTask(taskArgs []string) (retArgs []string, fixup func(*exec.Cmd), a 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]) } diff --git a/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile index 69424071..e63de6c9 100644 --- a/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile +++ b/docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile @@ -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 && \ 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 # 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 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 CXX=/opt/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++ ENV CGO_ENABLED=1 diff --git a/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile b/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile index deb2f147..249718ad 100644 --- a/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile +++ b/docker/android-armv8a-go1.23-qt6.6-dynamic.Dockerfile @@ -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 && \ 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: # 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 From 66e0973242c140ab329d7ec73aa25bcedeb3ff6b Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:48:36 +1200 Subject: [PATCH 16/26] examples/android: merge with helloworld[6] as they are now identical --- examples/android/main.go | 29 ------------------ examples/android/startup_android.go | 14 --------- examples/android/startup_other.go | 7 ----- examples/android6/main.go | 29 ------------------ examples/android6/startup_android.go | 14 --------- examples/android6/startup_other.go | 7 ----- .../helloworld.android.png} | Bin .../screenshot-android.png} | Bin 8 files changed, 100 deletions(-) delete mode 100644 examples/android/main.go delete mode 100644 examples/android/startup_android.go delete mode 100644 examples/android/startup_other.go delete mode 100644 examples/android6/main.go delete mode 100644 examples/android6/startup_android.go delete mode 100644 examples/android6/startup_other.go rename examples/{android/screenshot.png => helloworld/helloworld.android.png} (100%) rename examples/{android6/screenshot.png => helloworld6/screenshot-android.png} (100%) diff --git a/examples/android/main.go b/examples/android/main.go deleted file mode 100644 index 71455ef4..00000000 --- a/examples/android/main.go +++ /dev/null @@ -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!") -} diff --git a/examples/android/startup_android.go b/examples/android/startup_android.go deleted file mode 100644 index 2fbc4d5b..00000000 --- a/examples/android/startup_android.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build android - -package main - -import "C" // Required for export support - -//export AndroidMain -func AndroidMain() { - myRealMainFunc() -} - -func main() { - // Must be empty -} diff --git a/examples/android/startup_other.go b/examples/android/startup_other.go deleted file mode 100644 index d2382476..00000000 --- a/examples/android/startup_other.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !android - -package main - -func main() { - myRealMainFunc() -} diff --git a/examples/android6/main.go b/examples/android6/main.go deleted file mode 100644 index 7ce9703b..00000000 --- a/examples/android6/main.go +++ /dev/null @@ -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!") -} diff --git a/examples/android6/startup_android.go b/examples/android6/startup_android.go deleted file mode 100644 index 2fbc4d5b..00000000 --- a/examples/android6/startup_android.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build android - -package main - -import "C" // Required for export support - -//export AndroidMain -func AndroidMain() { - myRealMainFunc() -} - -func main() { - // Must be empty -} diff --git a/examples/android6/startup_other.go b/examples/android6/startup_other.go deleted file mode 100644 index d2382476..00000000 --- a/examples/android6/startup_other.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !android - -package main - -func main() { - myRealMainFunc() -} diff --git a/examples/android/screenshot.png b/examples/helloworld/helloworld.android.png similarity index 100% rename from examples/android/screenshot.png rename to examples/helloworld/helloworld.android.png diff --git a/examples/android6/screenshot.png b/examples/helloworld6/screenshot-android.png similarity index 100% rename from examples/android6/screenshot.png rename to examples/helloworld6/screenshot-android.png From 6d176bc410b1b4367e89871964e3eab8ce630607 Mon Sep 17 00:00:00 2001 From: mappu Date: Tue, 29 Apr 2025 22:48:58 +1200 Subject: [PATCH 17/26] miqt-docker: update usage text and README --- cmd/miqt-docker/README.md | 20 ++++++++++++++++---- cmd/miqt-docker/main.go | 23 +++++++++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/cmd/miqt-docker/README.md b/cmd/miqt-docker/README.md index be5b9b02..a57fea0f 100644 --- a/cmd/miqt-docker/README.md +++ b/cmd/miqt-docker/README.md @@ -2,6 +2,8 @@ This is a helper program to quickly run a dockerized MIQT build environment. +It also has some built-in commands that can be run either dockerized or natively. + - Supports 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 @@ -18,10 +20,19 @@ available embedded dockerfiles: ```bash Usage: miqt-docker ENVIRONMENT COMMAND... -Environment variables: -- DOCKER Override the path to docker +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: -Available container environments (use * for partial match): + -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) [...] ``` @@ -29,5 +40,6 @@ Example build commands: ```bash miqt-docker macos go build -ldflags '-s -w' -miqt-docker win64*qt6*dynamic go build -ldflags '-s -w -H windowsgui' +miqt-docker win64-qt6-dynamic go build -ldflags '-s -w -H windowsgui' +miqt-docker android-qt6 -android-build ``` diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index ea367fce..ffb1a100 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -37,12 +37,23 @@ func shasum(data []byte) string { // usage displays how to use miqt-docker and then exits the process. func usage(dockerfiles []fs.DirEntry) { - fmt.Fprintf(os.Stderr, "Usage: %s ENVIRONMENT COMMAND...\n", filepath.Base(os.Args[0])) - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, "Environment variables:") - fmt.Fprintln(os.Stderr, "- DOCKER Override the path to docker") - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, "Available container environments (use * for partial match):") + 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`)) From f0ce7984ced36f9578317b896079fb3af3566289 Mon Sep 17 00:00:00 2001 From: mappu Date: Wed, 30 Apr 2025 16:29:50 +1200 Subject: [PATCH 18/26] miqt-docker: windows compatibility --- cmd/miqt-docker/filepath.go | 16 +++++++++++++--- cmd/miqt-docker/main.go | 23 ++++++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cmd/miqt-docker/filepath.go b/cmd/miqt-docker/filepath.go index 5a5661c8..662aff57 100644 --- a/cmd/miqt-docker/filepath.go +++ b/cmd/miqt-docker/filepath.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "path/filepath" "runtime" "strings" @@ -16,6 +17,8 @@ func highestCommonParent(paths []string) (string, error) { parts := strings.Split(paths[0], string(filepath.Separator)) + caseSensitive := runtime.GOOS != "windows" + for _, check := range paths { checkn := strings.Split(check, string(filepath.Separator)) @@ -25,8 +28,15 @@ func highestCommonParent(paths []string) (string, error) { } for i, checkpart := range checkn[0:len(parts)] { // len(parts) is now <= len(checkn) so this is safe - if parts[i] == checkpart { - continue + if caseSensitive { + if parts[i] == checkpart { + continue + } + } else { + // case insensitive comparison + if strings.EqualFold(parts[i], checkpart) { + continue + } } // Divergence from i: onwards @@ -45,7 +55,7 @@ func highestCommonParent(paths []string) (string, error) { if isEmpty { if runtime.GOOS == "windows" { - return "", errors.New("Selected paths have no common ancestor") + return "", fmt.Errorf("Selected paths have no common ancestor: %v", paths) } return `/`, nil } diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index ffb1a100..5b9b2bb0 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "os/user" + "path" "path/filepath" "regexp" "runtime" @@ -224,15 +225,27 @@ func getDockerRunArgsForGlob(dockerfiles []fs.DirEntry, containerNameGlob string return nil, err } - mountDir := `/src/` + filepath.Base(cwd) // Don't use /src directly, otherwise -android-build will not know the package name for top-level builds + // 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) - fullCommand = append(fullCommand, `-v`, basedir+`:`+mountDir, `-w`, filepath.Join(mountDir, relCwd)) + 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:], `\`, `/`) - fullCommand = append(fullCommand, `-e`, `HOME=/tmp`) + // Always forwardslashes for in-docker paths, even on Windows OS + mountDir = strings.ReplaceAll(mountDir, `\`, `/`) + } - // Final standard docker commands + fullCommand = append(fullCommand, + `-v`, basedir+`:`+mountDir, + `-w`, path.Join(mountDir, relCwd), - fullCommand = append(fullCommand, containerName+`:`+dockerfileHash) // , `/bin/bash`, `-c`) + // Final standard docker commands + `-e`, `HOME=/tmp`, + containerName+`:`+dockerfileHash, + ) return fullCommand, nil } From a82e43585df62e7279f0292b07ee91f9efcc1721 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:19:51 +1200 Subject: [PATCH 19/26] miqt-docker: update README, simplify usage output --- cmd/miqt-docker/README.md | 42 ++++++++++++++++++++++++++++----------- cmd/miqt-docker/main.go | 4 ++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/cmd/miqt-docker/README.md b/cmd/miqt-docker/README.md index a57fea0f..f5dd6780 100644 --- a/cmd/miqt-docker/README.md +++ b/cmd/miqt-docker/README.md @@ -1,17 +1,10 @@ # miqt-docker -This is a helper program to quickly run a dockerized MIQT build environment. +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. -- Supports 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 -- 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 -- Handles using the proper uid+gid on Linux -- Automatically detect sudo requirement on Linux - ## Usage Run `miqt-docker` with no arguments to see full usage instructions and all @@ -21,7 +14,7 @@ available embedded dockerfiles: 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 special tasks: +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 @@ -33,13 +26,38 @@ 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 win64-qt6-dynamic go build -ldflags '-s -w -H windowsgui' +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 diff --git a/cmd/miqt-docker/main.go b/cmd/miqt-docker/main.go index 5b9b2bb0..6acf5abc 100644 --- a/cmd/miqt-docker/main.go +++ b/cmd/miqt-docker/main.go @@ -53,11 +53,11 @@ Environment variables: DOCKER Override the path to docker Available container environments: (use - as wildcard character) -- native (Run natively without docker) + native (Run natively without docker) `) for _, ff := range dockerfiles { - fmt.Fprintf(os.Stderr, "- %s\n", strings.TrimSuffix(ff.Name(), `.Dockerfile`)) + fmt.Fprintf(os.Stderr, " %s\n", strings.TrimSuffix(ff.Name(), `.Dockerfile`)) } os.Exit(1) From 901d2e0aa0cb83eb6f95947e28dcf00492d4646b Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:19:41 +1200 Subject: [PATCH 20/26] doc/README: add miqt-docker instructions --- README.md | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1132e8c3..101d39e5 100644 --- a/README.md +++ b/README.md @@ -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. +You can also try `miqt-docker native -minify-build` to use aggressive `CFLAGS`. + ### 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. @@ -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 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). @@ -132,6 +134,10 @@ You can replace the import path in two ways: 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 ### 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' ``` +### 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) *Tested with Fsu0413 Qt 5.15 / Clang 18.1 native compilation* @@ -297,37 +310,19 @@ For dynamic linking: 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 Qt.io Qt 6.6 / Android SDK 33 / Android NDK 25* +*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* 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: - - (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` +```bash +go install github.com/mappu/miqt/cmd/miqt-docker +miqt-docker android-qt5 -android-build # or android-qt6 +``` -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. From a01b7391baf80511b315ce6d08f1c8dd312da3b1 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:20:09 +1200 Subject: [PATCH 21/26] doc/README: use context-free docker builds, remove dockerignore --- .dockerignore | 33 --------------------------------- README.md | 6 +++--- 2 files changed, 3 insertions(+), 36 deletions(-) delete mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 4e76e597..00000000 --- a/.dockerignore +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 101d39e5..adc21248 100644 --- a/README.md +++ b/README.md @@ -249,14 +249,14 @@ Static linking is also available by installing the `mingw-w64-ucrt-x86_64-qt5-st For static linking: 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: - `docker run --rm -v $(pwd):/src -w /src miqt/win64-cross:latest go build --tags=windowsqtstatic -ldflags '-s -w -H windowsgui'` For dynamic linking: 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: - `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. @@ -303,7 +303,7 @@ Installing `qt@5` from Homebrew may be very slow if Homebrew chooses to do a fro For dynamic linking: 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: - `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. From 9747acc50bf023d19bcf3d84ad761cea583b93f7 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:20:21 +1200 Subject: [PATCH 22/26] doc/README: group into sections --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index adc21248..c20180e4 100644 --- a/README.md +++ b/README.md @@ -86,27 +86,27 @@ 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. -The `QByteArray`, `QString`, `QList`, `QVector`, `QMap`, `QHash` 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`, `QVector`, `QMap`, `QHash` 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 (�). 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. - -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()...)`. +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. +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. -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*)`. - - 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. Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future. From b55dd5e9c125f43cab857c75f9ce485bb9ef54ee Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:20:30 +1200 Subject: [PATCH 23/26] doc/README: add note re Android QFileDialog behaviour --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c20180e4..88455cee 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ 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. +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. From 95434b416ec4b1daa1dfc8c0176b7bf0287e7532 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 22:00:10 +1200 Subject: [PATCH 24/26] doc/README: the qt5-6 is not a module, so you can't use go mod edit --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 88455cee..cff99491 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,7 @@ You can use the `PKG_CONFIG_PATH` environment variable to override where CGO loo 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: -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"_' {} \;` +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"_' {} \;` ### Q9. How can I add bindings for another Qt library? From 2c4474690254fb77d57fb1ece0f61e587a900391 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:20:52 +1200 Subject: [PATCH 25/26] github/ci: simplify steps using miqt-docker, reinstate android-qt6 test --- .github/workflows/miqt.yml | 116 +++++++++++++++---------------------- 1 file changed, 46 insertions(+), 70 deletions(-) diff --git a/.github/workflows/miqt.yml b/.github/workflows/miqt.yml index 262fdfc7..79206bb2 100644 --- a/.github/workflows/miqt.yml +++ b/.github/workflows/miqt.yml @@ -39,8 +39,12 @@ jobs: path: ~/.cache/go-build key: linux64-buildall-gocache + # This uses the `genbindings` container in miqt-docker - name: Rebuild all libraries and examples 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: runs-on: ubuntu-24.04 @@ -49,18 +53,17 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Linux64 docker build - run: docker build -t miqt/linux64:qt5 -f docker/linux64-go1.19-qt5.15-dynamic.Dockerfile . - - name: Cache GOCACHE 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' - + - name: Linux64 bindings compile + run: | + make cmd/miqt-docker/miqt-docker + cd qt && ../cmd/miqt-docker/miqt-docker linux64-go1.19-qt5.15-dynamic go build + miqt_linux64_qt6_4: runs-on: ubuntu-24.04 @@ -68,9 +71,6 @@ jobs: - name: Checkout 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: @@ -78,7 +78,9 @@ jobs: key: linux64-qt64-gocache - 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: runs-on: ubuntu-24.04 @@ -87,9 +89,6 @@ jobs: - name: Checkout 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: @@ -97,7 +96,9 @@ jobs: key: linux64-qt68-gocache - 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: runs-on: ubuntu-24.04 @@ -112,11 +113,10 @@ jobs: 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 - 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: runs-on: ubuntu-24.04 @@ -131,11 +131,10 @@ jobs: 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 - 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: runs-on: ubuntu-24.04 @@ -150,11 +149,10 @@ jobs: 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 - 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: runs-on: ubuntu-24.04 @@ -163,57 +161,35 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Android armv8a docker build - run: docker build -t miqt/android:qt5 -f docker/android-armv8a-go1.23-qt5.15-dynamic.Dockerfile . - - name: Cache GOCACHE uses: actions/cache@v4 with: path: ~/.cache/go-build key: android-qt5-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/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: Android compile + 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: Android generate libRealAppName.so linking stub - 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 + miqt_android_qt6: + runs-on: ubuntu-24.04 - - name: Android generate json packaging metadata - run: docker run --rm -v $(pwd):/src -w /src/examples/android miqt/android:qt5 android-mktemplate.sh RealAppName deployment-settings.json + steps: + - name: Checkout + uses: actions/checkout@v4 - - 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: Cache GOCACHE + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: android-qt6-armv8a-gocache - - 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 + - name: Android compile + run: | + make cmd/miqt-docker/miqt-docker + cd examples/helloworld6 + ../../cmd/miqt-docker/miqt-docker android-armv8a-go1.23-qt6.6-dynamic -android-build + test -f helloworld6.apk From 9fc6ff9c2597392a8d9b4e0a6f4d8a3467d7e97c Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 1 May 2025 21:59:15 +1200 Subject: [PATCH 26/26] github/ci: remove GOCACHE steps that keep loading obsolete caches --- .github/workflows/miqt.yml | 61 ++++---------------------------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/.github/workflows/miqt.yml b/.github/workflows/miqt.yml index 79206bb2..635de256 100644 --- a/.github/workflows/miqt.yml +++ b/.github/workflows/miqt.yml @@ -14,6 +14,9 @@ jobs: - name: Checkout 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 uses: actions/cache@v4 with: @@ -33,12 +36,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: linux64-buildall-gocache - # This uses the `genbindings` container in miqt-docker - name: Rebuild all libraries and examples run: make build-all @@ -53,12 +50,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: linux64-qt5-gocache - - name: Linux64 bindings compile run: | make cmd/miqt-docker/miqt-docker @@ -71,12 +62,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: linux64-qt64-gocache - - name: Linux64 bindings compile run: | make cmd/miqt-docker/miqt-docker @@ -88,13 +73,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: linux64-qt68-gocache - + - name: Linux64 bindings compile run: | make cmd/miqt-docker/miqt-docker @@ -106,12 +85,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: win32-qt5-gocache - name: Win32 bindings compile run: | @@ -124,12 +97,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: win64-qt5-gocache - name: Win64 bindings compile run: | @@ -143,12 +110,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: win64-qt68-gocache - - name: Win64 bindings compile run: | make cmd/miqt-docker/miqt-docker @@ -160,13 +121,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: android-qt5-armv8a-gocache - + - name: Android compile run: | make cmd/miqt-docker/miqt-docker @@ -181,12 +136,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Cache GOCACHE - uses: actions/cache@v4 - with: - path: ~/.cache/go-build - key: android-qt6-armv8a-gocache - - name: Android compile run: | make cmd/miqt-docker/miqt-docker