bazelizing, bazelization, to bazelize, bazelized: The act of converting some existing software artifact to be easily consumed by the Bazel build tool

Introduction

I tried to build and run a Qt5 (5.15.2) application on macOS (10.15.7) using Bazel 5.0.0. Unfortunately, I run into some problems. The building part seems to work, but not the run part. In this post, I want to describe what I did and what are possible future directions and steps for this endeavor.

What I tried

I installed Qt5 on my machine using Homebrew:

brew install qt@5
brew link qt@5

It seems that building the application works, but not running it. I adapted https://github.com/justbuchanan/bazel_rules_qt/ to my needs. See this PR.

The change looks like this:

rom 36bbb59a7a06c618b50c01a0ae79c45e13cf9641 Mon Sep 17 00:00:00 2001
From: Vertexwahn <julian.amann@tum.de>
Date: Fri, 21 Jan 2022 22:49:29 +0100
Subject: [PATCH] Add macOS support

---
 .bazelci/presubmit.yml |  5 +++++
 qt.BUILD               | 16 ++++++++++++++++
 qt.bzl                 |  2 ++
 qt_configure.bzl       |  5 ++++-
 tools/BUILD            | 21 +++++++++++++++++++++
 tools/qt_toolchain.bzl |  1 +
 6 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index f6e8b7a..4a7e60d 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -6,3 +6,8 @@ tasks:
       - "sudo apt update && sudo apt -y install qt5-default qtdeclarative5-dev"
     build_targets:
       - "//..."
+  macos:
+    shell_commands:
+      - "sudo brew install qt@5"
+    build_targets:
+      - "//..."
diff --git a/qt.BUILD b/qt.BUILD
index 5081e8b..03e13ce 100644
--- a/qt.BUILD
+++ b/qt.BUILD
@@ -24,6 +24,21 @@ QT_LIBRARIES = [
     for name, include_folder, library_name, _ in QT_LIBRARIES
 ]

+[
+    cc_library(
+        name = "qt_%s_osx" % name,
+        # When being on Windows this glob will be empty
+        hdrs = glob(["%s/**" % include_folder], allow_empty = True),
+        includes = ["."],
+        linkopts = ["-F/usr/local/opt/qt5/Frameworks"] + [
+            "-framework %s" % library_name.replace("5", "") # macOS qt libs do not contain a 5 - e.g. instead of Qt5Core the lib is called QtCore
+            ],
+        # Available from Bazel 4.0.0
+        # target_compatible_with = ["@platforms//os:osx"],
+    )
+    for name, include_folder, library_name, _ in QT_LIBRARIES
+]
+
 [
     cc_import(
         name = "qt_%s_windows_import" % name,
@@ -57,6 +72,7 @@ QT_LIBRARIES = [
         deps = dependencies + select({
             "@platforms//os:linux": [":qt_%s_linux" % name],
             "@platforms//os:windows": [":qt_%s_windows" % name],
+            "@platforms//os:osx": [":qt_%s_osx" % name],
         }),
     )
     for name, _, _, dependencies in QT_LIBRARIES
diff --git a/qt.bzl b/qt.bzl
index 5f7db45..c719c55 100644
--- a/qt.bzl
+++ b/qt.bzl
@@ -161,10 +161,12 @@ def qt_cc_library(name, srcs, hdrs, normal_hdrs = [], deps = None, **kwargs):
             cmd = select({
                 "@platforms//os:linux": "moc $(location %s) -o $@ -f'%s'" % (hdr, header_path),
                 "@platforms//os:windows": "$(location @qt//:moc) $(locations %s) -o $@ -f'%s'" % (hdr, header_path),
+                "@platforms//os:osx": "$(location @qt//:moc) $(locations %s) -o $@ -f'%s'" % (hdr, header_path),
             }),
             tools = select({
                 "@platforms//os:linux": [],
                 "@platforms//os:windows": ["@qt//:moc"],
+                "@platforms//os:osx": ["@qt//:moc"],
             }),
         )
         _moc_srcs.append(":" + moc_name)
diff --git a/qt_configure.bzl b/qt_configure.bzl
index 7088125..8a7b030 100644
--- a/qt_configure.bzl
+++ b/qt_configure.bzl
@@ -33,6 +33,9 @@ def qt_autoconf_impl(repository_ctx):
         default_qt_path = "/usr/include/x86_64-linux-gnu/qt5"
         if not repository_ctx.path(default_qt_path).exists:
             default_qt_path = "/usr/include/qt"
+    elif os_name.find("mac") != -1:
+        # assume Qt was installed using `brew install qt@5`
+        default_qt_path = "/usr/local/opt/qt5"
     else:
         fail("Unsupported OS: %s" % os_name)

@@ -47,7 +50,7 @@ def qt_autoconf_impl(repository_ctx):
         qt_path_with_include = qt_path + "/include"
         if is_linux_machine and repository_ctx.path(qt_path_with_include).exists:
             qt_path = qt_path_with_include
-
+
     repository_ctx.file("BUILD", "# empty BUILD file so that bazel sees this as a valid package directory")
     repository_ctx.template(
         "local_qt.bzl",
diff --git a/tools/BUILD b/tools/BUILD
index 1896865..739fdd9 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -20,6 +20,14 @@ qt_toolchain(
     uic_path = "uic",
 )

+qt_toolchain(
+    name = "qt_osx",
+    # TODO: determine paths in qt_configure.bzl
+    moc_path = "/usr/local/opt/qt5/bin/moc",
+    rcc_path = "/usr/local/opt/qt5/bin/rcc",
+    uic_path = "/usr/local/opt/qt5/bin/uic",
+)
+
 toolchain(
     name = "qt_linux_toolchain",
     exec_compatible_with = [
@@ -45,3 +53,16 @@ toolchain(
     toolchain = ":qt_windows",
     toolchain_type = "@com_justbuchanan_rules_qt//tools:toolchain_type",
 )
+
+toolchain(
+    name = "qt_osx_toolchain",
+    exec_compatible_with = [
+        "@platforms//os:osx",
+    ],
+    target_compatible_with = [
+        "@platforms//os:osx",
+        "@platforms//cpu:x86_64",
+    ],
+    toolchain = ":qt_osx",
+    toolchain_type = "@com_justbuchanan_rules_qt//tools:toolchain_type",
+)
diff --git a/tools/qt_toolchain.bzl b/tools/qt_toolchain.bzl
index 9c984c9..9148ed0 100644
--- a/tools/qt_toolchain.bzl
+++ b/tools/qt_toolchain.bzl
@@ -26,4 +26,5 @@ def register_qt_toolchains():
     native.register_toolchains(
         "@com_justbuchanan_rules_qt//tools:qt_linux_toolchain",
         "@com_justbuchanan_rules_qt//tools:qt_windows_toolchain",
+        "@com_justbuchanan_rules_qt//tools:qt_osx_toolchain",
     )
--

When I try to run:

bazel run --cxxopt=-std=c++17 //tests/qt_resource:main

I receive the runtime error:

dyld: Symbol not found: __ZN10QByteArray6_emptyE

Nevertheless, building everything using bazel build --cxxopt=-std=c++17 //... seems to work. I am not 100% sure if the link options -F/usr/local/opt/qt5/Frameworks and -framework QtCore, etc. are correct.

For me, it is a bit unclear what dependencies the main binary expects. I tried to copy QtCore.framework to the location of the main binary manually but this does not change the error message.

What files does the main binary expect?

Steps to reproduce the issue:

bash
# brew install bazel # Install Bazel
# brew install qt@5  # Install Qt5
git clone https://github.com/Vertexwahn/bazel_rules_qt.git
cd bazel_rules_qt
git checkout add-macos-support
bazel build --cxxopt=-std=c++17 //... # should work
bazel run --cxxopt=-std=c++17 //tests/qt_resource:main # should give you the error message

If I try to run macdeployqt on my main binary I get also some errors. I do within my workspace root dir a cd bazel-bin/tests/qt_resource and run then /usr/local/opt/qt5/bin/macdeployqt main:

ERROR: Could not find bundle binary for "main"
ERROR: "error: /Library/Developer/CommandLineTools/usr/bin/otool-classic: can't open file:  (No such file or directory)\n"
ERROR: "error: /Library/Developer/CommandLineTools/usr/bin/otool-classic: can't open file:  (No such file or directory)\n"
ERROR: "error: /Library/Developer/CommandLineTools/usr/bin/otool-classic: can't open file:  (No such file or directory)\n"
WARNING:
WARNING: Could not find any external Qt frameworks to deploy in "main"
WARNING: Perhaps macdeployqt was already used on "main" ?
WARNING: If so, you will need to rebuild "main" before trying again.
ERROR: Could not find bundle binary for "main"
ERROR: "error: /Library/Developer/CommandLineTools/usr/bin/strip: can't open file:  (No such file or directory)\n"
ERROR: ""

My hope was that macdeployqt would collect all needed resources for me, but this seems not to work.

If I convert my main to an app via lipo -create -output universall_app main and do then a /usr/local/opt/qt5/bin/macdeployqt universall_app I get the same error message.

I posted als a StackOverflow question about this.

The CMake approach

To make sure that there is no general problem with my system setup I tried to use CMake to build a Qt5 application:

git clone https://github.com/euler0/mini-cmake-qt.git
cd mini-cmake-qt
git checkout qt5
cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5 .
make -j

This produces an example.app. With a double click on this application bundle, the application can be started. This worked on my system.

Future directions

It seems that rules_apple can be used to create an application bundle. I am not sure if I need to transform my Qt application binary to an app bundle to b able to execute it. One could use --sandbox_debugto identify what Bazel is doing and a kind of dtrace for the CMake version to compare the differences. I am currently not sure what trying to do next and hope for an easy solution. A Qt6 solution would also be fine for me.

The dtrace idea would work like this:

csrutil status # Make sure SIP is deactivated 
sudo dtruss make -j

In the long term it would be desirable to have Bazel rules for Qt (Qt6) that one can use out of the box, without the need to preinstall anything. An attempt into this direction was made for example here.

UPDATE: Meanwhile I got the problem fixed and it went into the “official” Qt Bazel rules repository.