Now it’s the end of Google Summer of Code 2019. As my GSoC project, the port of KDE Connect on macOS has made great progress. You can find and download it in my blog release page.
Note: This post aims at presenting the features of KDE Connect which have been implemented on macOS. If you’d like to know more information, such as compilation of your own KDE Connect binary on macOS, please turn to another post in my post Connect your Android phone with your Mac via KDE Connect. And if you’re interested in what I’ve done during Google Summer of Code, my status report of Google Summer of Code is HERE.
Features
In this chapter, I’d like to give you a preview of all features, as well as how to configure to make some of functions work.
Launch KDE Connect
First, we can click on KDE Connect application - the kdeconnect-indicator.app to open it.
Then, we can open KDE Connect configuration window from the indicator in the tray bar of macOS.
As you can see, this is the main page of KDE Connect. All available plugins are here, you can enable/disable or configure them. In addition, available devices will be listed on the left, you can choose them to pair/unpair with them/it.
Functions
Pair notification
When you pair from your Andoid Phone, you should be able to receive a notification that shows the pair request. You can accept or reject it in the KDE Connect configuration window, or you can do it with KDE Connect indicator tray icon, there would be an entry for the pair request as well.
Otherwise, if you change the notification type of KDE Connect to alert in the system preference, you should also be able to do a quick action with the notification itself. Just as I showed in Enable notification plugin in KDE Connect on macOS.
Once paired, you can enjoy your adventure on macOS with KDE Connect!
Clipboard synchronization
The text that you copy on your Mac will be shared to your phone, and those you copy on your phone will be also synchronized to your Mac.
Notification synchronization
With KNotifications support for macOS, you can receive notification from your Android phones and react to them. You can ping your Mac to test whether they are well connected.
Sending file
Sharing your file on your Mac with your Android phone is also a basic feature. You could also send a file from your Android phone, by default, the file will be saved in the Downloads folder in your Mac.
System Volume
You can control the system value of your Mac from your Android Phone remotely.
SFTP
With my SFTP browser, you can browse files in your Android Phone from your Mac, easily synchronize a file.
SMS
Thanks to SMS application of Simon Redman, sending and receiving SMS on your Mac are possible!
Running command
Run command from your Android phone. I believe that using AppleScript, more and more things that KDE Connect can do on macOS, will be discovered, maybe by you!
Mouse and Keyboard
You should be able to use your Android phone as a temporary trackpad and a keyboard. But it needs your permission to allow your Android phone to do it on your Mac. The GIF above shows how to do that.
Others
Except the functions shown above, you can also do these from your Android phone:
Keep your Mac awake when your phone is connected
Use your phone to control your slides during a presentation
Check the battery level of your phone
Ring your phone to help find it
And, you may have noticed that, in the screen capture, there are KDE Connect in dark mode and in light mode. Thanks to Qt, we are able to benefit it.
Furthermore, there is no doubt that more functions will be delivered and released in the future. We are all looking forward to them.
Issues
There are some issues that we’ve known and we are trying to fix them.
The released application package isn’t notarized and still has some lirary reference issues. So, it requires you to manually open it, if it’s rejected by Gatekeeper(package validator on macOS), like that showed in the image above.
We’ll try to fix all issues and make a release which you can run it without barricade.
Acknowledgement
Thanks to KDE Community and Google, I could finish this Google Summer of Code project this summer.
Thanks to members in KDE Connect development. Without them, I cannnot understand the mechanism and get it work on macOS so quickly :)
Conclusion
If you have any question, KDE Connect Wiki may be helpful. And you can find a bug tracker there.
Don’t be hesitated to join our Telegram Group or IRC channel if you’d like to bring more exciting functions into KDE Connect:
DBus is a concept of software bus, an inter-process communication (IPC), and a remote procedure call (RPC) mechanism that allows communication between multiple computer programs (that is, processes) concurrently running on the same machine. DBus was developed as part of the freedesktop.org project, initiated by Havoc Pennington from Red Hat to standardize services provided by Linux desktop environments such as GNOME and KDE.
In this post, we only talk about how does DBus daemon run and how KDE Applications/Frameworks connect to it. For more details of DBus itself, please move to DBus Wiki.
QDBus
There are two types of bus: session bus and system bus. The user-end applications should use session bus for IPC or RPC.
For the DBus connection, there is already a good enough library named QDBus provided by Qt. Qt framework and especially QDBus is widely used in KDE Applications and Frameworks on Linux.
A mostly used function is QDBusConnection::sessionBus() to establish a connection to default session DBus. All DBus connection are established through this function.
Its implementation is:
1 2 3 4 5 6
QDBusConnection QDBusConnection::sessionBus() { if (_q_manager.isDestroyed()) return QDBusConnection(nullptr); return QDBusConnection(_q_manager()->busConnection(SessionBus)); }
where _q_manager is an instance of QDBusConnectionManager.
QDBusConnectionManager is a private class so that we don’t know what exactly happens in the implementation.
On macOS, we don’t have a pre-installed dbus. When we compile it from source code, or install it from HomeBrew or somewhere, a configuration file session.conf and a launchd configuration file org.freedesktop.dbus-session.plist are delivered and expected to install into the system.
session.conf
In session.conf, one important thing is <listen>launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET</listen>, which means socket path should be provided by launchd through the environment DBUS_LAUNCHD_SESSION_BUS_SOCKET.
org.freedesktop.dbus-session.plist
On macOS, launchd is a unified operating system service management framework, starts, stops and manages daemons, applications, processes, and scripts. Just like systemd on Linux.
The file org.freedesktop.dbus-session.plist describes how launchd can find a daemon executable, the arguments to launch it, and the socket to communicate after launching daemon.
--address=unix:tmpdir=/tmp provides a base directory to store a random unix socket descriptor. So we could have serveral instances at the same time, with different addresse.
--print-address can let dbus-daemon write its generated, real address into standard output.
Then we redirect the output of dbus-daemon to KdeConnectConfig::instance()->privateDBusAddressPath(). Normally, it should be $HOME/Library/Preferences/kdeconnect/private_dbus_address. For example, the address in it is unix:path=/tmp/dbus-K0TrkEKiEB,guid=27b519a52f4f9abdcb8848165d3733a6.
Therefore, our program can access this file to get the real DBus address, and use another function in QDBus to connect to it:
We redirect all QDBusConnection::sessionBus to QDBusConnection::connectToBus to connect to our own DBus.
Fake a session DBus
With such solution, kdeconnectd and kdeconnect-indicator coworks well. But in KFrameworks, there are lots of components which are using QDBusConnection::sessionBus rather than QDBusConnection::connectToBus. We cannot change all of them.
Then I came up with an idea, try to fake a session bus on macOS.
To hack and validate, I tried to launch a dbus-daemon using /tmp/dbus-K0TrkEKiEB as address, and then I tried type this in my terminal:
Then I launched dbus-monitor --session. It did connect to the bus that I launched.
And then, any QDBusConnection::sessionBus can establish a stable connection to the faked session bus. So components in KFramework can use the same session bus as well.
To implement it in KDE Connect, after starting dbus-daemon, I read the file content, filter the socket address, and call launchctl to set DBUS_LAUNCHD_SESSION_BUS_SOCKET env.
Since we can directly use session bus, the redirect from QDBusConnection::sessionBus to QDBusConnection::connectToBus is not necessary anymore. Everyone can connect it in convenience.
Each time we launch kdeconnectd, a new dbus-daemon is launched and the environment in launchctl is overwritten. To improve this, we might detect whether there is already an available dbus-daemon through testing connectivity of returned QDBusConnection::sessionBus. This might be done by a bootstrap script.
It will be really nice if we can have a unified way for all KDE Applications on macOS.
Conclusion
I’m looking forward to a general DBus solution for all KDE applications :)
As I mentioned, this post will help you to build your own KDE Connect with native Notification support for macOS.
Build
This post will not give you instructions of building KDE Connect on macOS because there is already a page on KDE Connect Wiki
If you met any problems, you can submit them on our KDE bug tracker
Add notification support
Notification plugin depends on KNotification. There is no native support for macOS in this library.
I’ve made a native one and it has been submited as a patch. But it takes time to get reviewed and optimized.
I keep the patch available on a repo of my GitHub: https://github.com/Inokinoki/knotifications. So, Craft can access it and compile it to provide support of macOS Notification.
But we’re looking forward to its delivery in KNotification.
What you need to do is very simple:
Find KNotifications blueprint file
Enter your CraftRoot folder. To me, it’s /Users/inoki/CraftRoot.
Enter etc -> blueprints -> locations -> craft-blueprints-kde folder.
Open kde/frameworks/tier3/knotifications/knotifications.py.
Remove self.versionInfo.setDefaultValues() in setTargets of subinfo class. If you’re not familiar with python, just find this line and delete it.
You could receive notifications from your phone or your other computers(if well configured), just like this:
You can also change notification settings of KDE Connect in your macOS Notification Center. By default, the notification style is Bar, set it to Alert to see quick actions to your notifications.
Notice: Currently there is a bug, you may receive duplicated notifications. We’re figuring out its reason and it will be fixed as soon as possible.
Thanks for your reading and your support to KDE Connect :)
If you’d like to, you can also follow me on GitHub :)
For pros
For developers, if you’re familiar with diff, just apply this diff patch:
Have you ever heard Continuity, the solution of Apple which provides one seamless experience between your iPhone and your Mac?
You may be surprised, “Woohoo, it’s amazing but I use my OnePlus along with my Mac.” With my GSoC 2019 project, you can connect your Mac and your Android phone with KDE Connect!
And you can even connect your Mac with your Linux PC or Windows PC (Thanks to Piyush, he is working on optimizing experience of KDE Connect on Windows).
Installation instruction
You can download KDE Connect Nightly Build for macOS from KDE Binary Factory: https://binary-factory.kde.org/view/MacOS/job/kdeconnect-kde_Nightly_macos/. But notice that it’s not yet a stable version, and it requires that you have permission to run application from non-certificated developer. We’ll release a stable one next month on August.
Otherwise you can build your own version. Please follow the instructions on KDE Connect Wiki. If you’re using macOS 10.13, MacOS X 10.12 or below, we recommend that you build your own KDE Connect because our Binary Factory are building applications for only macOS 10.14 or above.
You’ll finally get a DMG image file in both 2 ways.
Just click on it, mount it and drap kdeconnect-indicator into Applications folder.
Open kdeconnect-indicator and your magic journey with KDE Connect for macOS begins!
Use
After installation, you can see an icon of kdeconnect-indicator in the Launchpad.
Click it to open. If everything is ok, you will see an KDE Connect icon in your system tray.
Click the icon -> Configure to open configuration window. Here you can see discovered devices and paired devices.
You can enable or disable functions in this window.
Currently, you can do these from your Android phone:
Run predefined commands on your Mac from connected devices.
Check your phones battery level from the desktop
Ring your phone to help finding it
Share files and links between devices
Control the volume of your Mac from the phone
Keep your Mac awake when your phone is connected
Receive your phone notifications on your desktop computer (this function is achieved but not yet delivered, you can follow this post to enable it manually)
I’m trying to make more plugins work on macOS. Good luck to my GSoC project :)
Acknowledgement
Thanks to KDE Community and Google, I could start this Google Summer of Code project this summer.
Thanks to members in KDE Connect development. Without them, I cannnot understand the mechanism and get it work on macOS so quickly :)
Conclusion
If you have any question, KDE Connect Wiki may be helpful. And you can find a bug tracker there.
Don’t be hesitated to join our Telegram Group or IRC channel if you’d like to bring more exciting functions into KDE Connect:
In Craft, to create a package, we can use craft --package <blueprint-name> after the compiling and the installing of a library or an application with given blueprint name.
On macOS, MacDMGPackager is the packager used by Craft. The MacDylibBundleris used in MacDMGPackager to handle the dependencies.
In this article, I’ll give a brief introduction of the two classes and the improvement which I’ve done for my GSoC project.
MacDMGPackager
MacDMGPackager is a subclass of CollectionPackagerBase. Its most important method is createPackage.
Here we get the definitions, the path of the application which we want to pack, and the path of archive. The appPath should be the root of an application package with .app extension name. According to the convention of applications on macOS, targetLibdir points to the library directory of the application. During the compiling and the installing period, in the application directory, there is only a .plist and MacOS subdirectory. So next, the library directory is created for further using.
Then, we predefine a list of pairs of source and destination for directories and move the files to the destinations. The destionations are the correct directories of libraries, plugins and resources in a macOS application package.
for src, dest in moveTargets: if os.path.exists(src): ifnot utils.mergeTree(src, dest): returnFalse
Fixing dependencies using MacDylibBundler
After the moving, we create an instance of MacDylibBundler with appPath. After the with instruction, all the codes are executed with DYLD_FALLBACK_LIBRARY_PATH=<package.app>/Contents/Frameworks:<Craft-Root>/lib environment variable.
For further reading of this environment variable, please refer this question on StackOverFlow.
Here, we firstly create an object of Path. It points to the executable of macOS Package.
It should be reminded that, although here, we use the same name for both the macOS application package and the executable, it is not mandatory. The name of executable is defined by CFBundleExecutable in the .plist file. So maybe read it from the .plist file is a better solution.
Then, the method bundleLibraryDependencies is used to copy libraries and fix dependencies for the executable in the package.
A brief introduction of this method:
Call utils.getLibraryDeps for getting a list of dependencies. This operation is done by using otool -L.
Copy missing dependencies into Contents/Frameworks, and update the library information in the executable. I’ll give an analyse in detail in the next chapter.
And then, we try to fix all the dependencies of libraries in Contents/Frameworks and Contents/PlugIns.
1 2 3 4 5 6 7
# Fix up the library dependencies of files in Contents/Frameworks/ CraftCore.log.info("Bundling library dependencies...") ifnot dylibbundler.fixupAndBundleLibsRecursively("Contents/Frameworks"): returnFalse CraftCore.log.info("Bundling plugin dependencies...") ifnot dylibbundler.fixupAndBundleLibsRecursively("Contents/PlugIns"): returnFalse
Fixing dependencies using macdeployqt
The macdeployqt is used to fix the Qt libraries used by the application. Craft installed it while compiling and installing Qt. But don’t worry, it is not in your system path.
I have not yet found what macdeployqt exactly do, it’s nice to have an look at its source code.
If macdeplyqt added some files which we don’t want, they would be removed here.
1 2 3 4 5 6 7 8 9
# macdeployqt might just have added some explicitly blacklisted files blackList = Path(self.packageDir(), "mac_blacklist.txt") if blackList.exists(): pattern = [self.read_blacklist(str(blackList))] # use it as whitelist as we want only matches, ignore all others matches = utils.filterDirectoryContent(appPath, whitelist=lambda x, root: utils.regexFileFilter(x, root, pattern), blacklist=lambda x, root:True) for f in matches: CraftCore.log.info(f"Remove blacklisted file: {f}") utils.deleteFile(f)
Fixing dependencies after fixing of macdeployqt
After macdeplotqt, there may be some libraries or plugins added by macdeplotqt. So we do the fixing of dependencies once again.
But I’m doubting if we need to fix twice the dependencies. I’ll update this post after I figure out what will it lead to if we fust fix after macdeployqt.
1 2 3 4 5 6 7 8 9
# macdeployqt adds some more plugins so we fix the plugins after calling macdeployqt dylibbundler.checkedLibs = set() # ensure we check all libs again (but # we should not need to make any changes) CraftCore.log.info("Fixing plugin dependencies after macdeployqt...") ifnot dylibbundler.fixupAndBundleLibsRecursively("Contents/PlugIns"): returnFalse CraftCore.log.info("Fixing library dependencies after macdeployqt...") ifnot dylibbundler.fixupAndBundleLibsRecursively("Contents/Frameworks"): returnFalse
Checking dependencies
Then, we use MacDylibBundler to check all dependencies in the application package. If there is any bad dependency, the package process will fail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Finally sanity check that we don't depend on absolute paths from the builder CraftCore.log.info("Checking for absolute library paths in package...") found_bad_dylib = False# Don't exit immeditately so that we log all the bad libraries before failing: ifnot dylibbundler.areLibraryDepsOkay(mainBinary): found_bad_dylib = True CraftCore.log.error("Found bad library dependency in main binary %s", mainBinary) ifnot dylibbundler.checkLibraryDepsRecursively("Contents/Frameworks"): CraftCore.log.error("Found bad library dependency in bundled libraries") found_bad_dylib = True ifnot dylibbundler.checkLibraryDepsRecursively("Contents/PlugIns"): CraftCore.log.error("Found bad library dependency in bundled plugins") found_bad_dylib = True if found_bad_dylib: CraftCore.log.error("Cannot not create .dmg since the .app contains a bad library depenency!") returnFalse
Creating DMG image
Up to now, everything is well, we can create a DMG image for the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
name = self.binaryArchiveName(fileType="", includeRevision=True) dmgDest = os.path.join(self.packageDestinationDir(), f"{name}.dmg") if os.path.exists(dmgDest): utils.deleteFile(dmgDest) appName = defines['appname'] + ".app" ifnot utils.system(["create-dmg", "--volname", name, # Add a drop link to /Applications: "--icon", appName, "140", "150", "--app-drop-link", "350", "150", dmgDest, appPath]): returnFalse
CraftHash.createDigestFiles(dmgDest)
returnTrue
An example of DMG image is like this one, users can drag the application into Applications directory to install it.
MacDylibBundler
Constructor
1 2 3 4
def__init__(self, appPath: str): # Avoid processing the same file more than once self.checkedLibs = set() self.appPath = appPath
In the constructor, a set is created to store the libraries which have been already checked. And the appPath passed by developer is stored.
Methods
This method bundleLibraryDependencies and _addLibToAppImage are the most important methods in this class. But they’re too long. So I’ll only give some brief introduction of them.
_addLibToAppImage checks whether a library is already in the Contents/Frameworks. If the library doesn’t exist, it copies it into the diretory and tries to fix it with some relative path.
bundleLibraryDependencies checks the dependencies of fileToFix. If there are some dependencies with absolute path, it copies the dependencies into Contents/Frameworks by calling _addLibToAppImage. And then, it calls _updateLibraryReference to update the reference of library.
As description in the docstring, fixupAndBundleLibsRecursively can remove absolute references and budle all depedencies for all dylibs.
It traverses the directory, and for each file which is not symbol link, checks whether it ends with “.so” or “.dylib”, or there is “.so.” in the file name, or there is “.framework” in the full path and it’s a macOS binary. If it’s that case, call bundleLibraryDependencies method to bundle it in to .app package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
deffixupAndBundleLibsRecursively(self, subdir: str): """Remove absolute references and budle all depedencies for all dylibs under :p subdir""" # ... for dirpath, dirs, files in os.walk(os.path.join(self.appPath, subdir)): for filename in files: fullpath = Path(dirpath, filename) if fullpath.is_symlink(): continue# No need to update symlinks since we will process the target eventually. if (filename.endswith(".so") or filename.endswith(".dylib") or".so."in filename or (f"{fullpath.name}.framework"in str(fullpath) and utils.isBinary(str(fullpath)))): ifnot self.bundleLibraryDependencies(fullpath): CraftCore.log.info("Failed to bundle dependencies for '%s'", os.path.join(dirpath, filename)) returnFalse # ...
areLibraryDepsOkay can detect all the dependencies. If the library is not in @rpath, @executable_path or system library path, the dependencies cannot be satisfied on every mac. It may work relevant to the environment. But it will be a big problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
defareLibraryDepsOkay(self, fullPath: Path): # ... for dep in utils.getLibraryDeps(str(fullPath)): if dep == libraryId andnot os.path.isabs(libraryId): continue# non-absolute library id is fine # @rpath and @executable_path is fine if dep.startswith("@rpath") or dep.startswith("@executable_path"): continue # Also allow /System/Library/Frameworks/ and /usr/lib: if dep.startswith("/usr/lib/") or dep.startswith("/System/Library/Frameworks/"): continue if dep.startswith(CraftStandardDirs.craftRoot()): CraftCore.log.error("ERROR: %s references absolute library path from craftroot: %s", relativePath, dep) elif dep.startswith("/"): CraftCore.log.error("ERROR: %s references absolute library path: %s", relativePath, dep) else: CraftCore.log.error("ERROR: %s has bad dependency: %s", relativePath, dep) found_bad_lib = True
Here, in checkLibraryDepsRecursively, we traverse the directory to check all the dependencies of libraries, which is .dylib or .so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
defcheckLibraryDepsRecursively(self, subdir: str): # ... for dirpath, dirs, files in os.walk(os.path.join(self.appPath, subdir)): for filename in files: fullpath = Path(dirpath, filename) if fullpath.is_symlink() andnot fullpath.exists(): CraftCore.log.error("Found broken symlink '%s' (%s)", fullpath, os.readlink(str(fullpath))) foundError = True continue
if filename.endswith(".so") or filename.endswith(".dylib") or".so."in filename: ifnot self.areLibraryDepsOkay(fullpath): CraftCore.log.error("Found library dependency error in '%s'", fullpath) foundError = True # ...
Static methods in class
The _updateLibraryReference method can use install_name_tool -change command to change a reference of dynamic library in a macOS/BSD binary.
1 2 3 4 5 6 7 8 9 10 11
@staticmethod def_updateLibraryReference(fileToFix: Path, oldRef: str, newRef: str = None) -> bool: if newRef isNone: basename = os.path.basename(oldRef) newRef = "@executable_path/../Frameworks/" + basename with utils.makeWritable(fileToFix): ifnot utils.system(["install_name_tool", "-change", oldRef, newRef, str(fileToFix)], logCommand=False): CraftCore.log.error("%s: failed to update library dependency path from '%s' to '%s'", fileToFix, oldRef, newRef) returnFalse returnTrue
The _getLibraryNameId method can use otool -D to get the identity of a dynamic library in a macOS/BSD binary.
1 2 3 4 5 6 7 8 9 10
@staticmethod def_getLibraryNameId(fileToFix: Path) -> str: libraryIdOutput = io.StringIO( subprocess.check_output(["otool", "-D", str(fileToFix)]).decode("utf-8").strip()) lines = libraryIdOutput.readlines() if len(lines) == 1: return"" # Should have exactly one line with the id now assert len(lines) == 2, lines return lines[1].strip()
The _fixupLibraryId method can use install_name_tool -id to try to fix the absolute identity of a dynamic library in a macOS/BSD binary.
1 2 3 4 5 6 7 8 9 10 11
@classmethod def_fixupLibraryId(cls, fileToFix: Path): libraryId = cls._getLibraryNameId(fileToFix) if libraryId and os.path.isabs(libraryId): CraftCore.log.debug("Fixing library id name for %s", libraryId) with utils.makeWritable(fileToFix): ifnot utils.system(["install_name_tool", "-id", os.path.basename(libraryId), str(fileToFix)], logCommand=False): CraftCore.log.error("%s: failed to fix absolute library id name for", fileToFix) returnFalse # ...
Conclusion
This class is a magic class which can achieve almost everything on macOS.
But the code style is a little confusing. And the parameters are not agreed. Some methods use str to represent a path, some use Path.
Maybe this can be also improved in the future.
Anyway, it’s really a helpful class.
Improvement
During my bonding period, I found that there is a library named qca-qt5 is not fixed appropriately. It caused a crash.
Locating the problem
After analyzing of crash log, I found that the library qca-qt5 is loaded twice. Two libraries with same dynamic library id caused this crash.
One is in the .app package, the other is in CraftRoot/lib.
As far as I know, qca-qt5 tried to search its plugins in some path. The one in the package is not fixed, so it started a searching of plugins in the CraftRoot/lib directory. The plugins in it refer the qca-qt5 in the directory. So the two libraries with the same name are loaded, and the application crashed.
Cause
With good knowing of MacDylibBundler, we can improve it to fix the bug. And this will be helpful to other applications or libraries in Craft.
I noticed that all the libraries with .dylib can be handled correctly. The problem is based on the libraries in the .framework package. It seems that Craft cannot handle the dynamic libraries in the .framework correctly.
And we can see that, in checkLibraryDepsRecursively, only .so and .dylib are checked. So this is a bug covered deeply.
1 2 3 4 5 6 7
CRAFT: ➜ MacOS otool -L kdeconnectd kdeconnectd: /Volumes/Storage/Inoki/CraftRoot/lib/libkdeconnectcore.1.dylib (compatibility version 1.0.0, current version 1.3.3) /Volumes/Storage/Inoki/CraftRoot/lib/libKF5KIOWidgets.5.dylib (compatibility version 5.0.0, current version 5.57.0) /Volumes/Storage/Inoki/CraftRoot/lib/libKF5Notifications.5.dylib (compatibility version 5.0.0, current version 5.57.0) /Volumes/Storage/Inoki/CraftRoot/lib/qca-qt5.framework Versions/2.2.0/qca-qt5 (compatibility version 2.0.0, current version 2.2.0) ...
In the _addLibToAppImage method, the library in the framework is copied directly to the Contents/Frameworks. For example, lib/qca-qt5.framework/Versions/2.2.0/qca-qt5 becomes Contents/Frameworks/qca-qt5.
And then, during the fix in fixupAndBundleLibsRecursively method, according to the following code, it will not be fixed. Although it should be in a .framework directory and it’s a binary, after _addLibToAppImage, it will not be in a .framework directory. So it will not be fixed.
1 2 3 4 5 6 7
if (filename.endswith(".so") or filename.endswith(".dylib") or".so."in filename or (f"{fullpath.name}.framework"in str(fullpath) and utils.isBinary(str(fullpath)))): ifnot self.bundleLibraryDependencies(fullpath): CraftCore.log.info("Failed to bundle dependencies for '%s'", os.path.join(dirpath, filename)) returnFalse
Fixing it !
To fix it, I think a good idea is copying all the .framework directory and keeping its structure.
I firstly do a checking in the _addLibToAppImage method. For example, if qca-qt5 is in the qca-qt5.framework subdirectory, we change the libBasename to qca-qt5.framework/Versions/2.2.0/qca-qt5. So the targetPath can also be updated correctly.
1 2 3 4 5 6 7 8 9
libBasename = libPath.name
# Handle dylib in framework iff"{libPath.name}.framework"in str(libPath): libBasename = str(libPath)[str(libPath).find(f"{libPath.name}.framework"):]
targetPath = Path(self.appPath, "Contents/Frameworks/", libBasename) if targetPath.exists() and targetPath in self.checkedLibs: returnTrue
After several checkings, an important section is copying the library. I add some code to check if the library is in a .framework directory. If a library is in a .framework directory, I try to copy the entire directory to the Contents/Frameworks. So for qca-qt5, it should be Contents/Frameworks/qca-qt5.framework/Versions/2.2.0/qca-qt5.
After copying, another important point is in _updateLibraryReference. If a library is in a .framework directory, the new reference should be @executable_path/../Frameworks/*.framework/....
After fixing, the executable can be launched without crash.
1 2 3 4 5 6 7 8 9 10 11 12
CRAFT: ➜ MacOS otool -L kdeconnectd kdeconnectd: @executable_path/../Frameworks/libkdeconnectcore.1.dylib (compatibility version 1.0.0, current version 1.3.3) @executable_path/../Frameworks/libKF5KIOWidgets.5.dylib (compatibility version 5.0.0, current version 5.57.0) @executable_path/../Frameworks/libKF5Notifications.5.dylib (compatibility version 5.0.0, current version 5.57.0) @executable_path/../Frameworks/qca-qt5.framework/Versions/2.2.0/qca-qt5 (compatibility version 2.0.0, current version 2.2.0) ... CRAFT: ➜ MacOS ./kdeconnectd kdeconnect.core: KdeConnect daemon starting kdeconnect.core: onStart kdeconnect.core: KdeConnect daemon started kdeconnect.core: Broadcasting identity packet
Conclusion
In the software development, there are always some cases which we cannot consider. Open Source gives us the possibility of collecting intelligence from people all over the world to handle such cases.
That’s also why I like Open Source so much.
Today is the first day of coding period, I hope all goes well for the community and all GSoC students :)
Lacking VLC and libvlc in Craft, phonon-vlc cannot be built successfully on macOS. It caused the failed building of KDE Connect in Craft.
As a small step of my GSoC project, I managed to build KDE Connect by removing the phonon-vlc dependency. But it’s not a good solution. I should try to fix phonon-vlc building on macOS. So during the community bonding period, to know better the community and some important tools in the Community, I tried to fix phonon-vlc.
Fixing phonon-vlc
At first, I installed libVLC in MacPorts. All Header files and libraries are installed into the system path. So theoretically, there should not be a problem of the building of phonon-vlc. But an error occurred:
We can figure that the compiling is ok, the error is just at the end, during the linking. The error message tells us there is no QtDBus lib. So to fix it, I made a small patch to add QtDBus manually in the CMakeLists file.
A small problem is that Hannah said she didn’t get an error during linking. It may be something about Qt version. If someone gets some idea, welcome to contact me.
My Qt version is 5.12.3.
Fixing VLC
To fix VLC, I tried to pack the VLC binary just like the one on Windows.
But unfortunately, in the .app package, the Header files are not completed. Comparing to Windows version, the entire plugins folder is missing.
So I made a patch for all those files. But the patch is too huge (25000 lines!). So it is not a good idea to merge it into master branch.
Thanks to Hannah, she has made a libs/vlc blueprint in the master branch, so in Craft, feel free to install it by running craft libs/vlc.
Troubleshooting
If you cannot build libs/vlc, just like me, you can also choose the binary version VLC with Header files patch.
To use it, run craft --add-blueprint-repository https://github.com/inokinoki/craft-blueprints-inoki.git and the blueprint(s) will be added into your local blueprint directory.
Then, craft binary/vlc will help get the vlc binary and install Header files, libraries into Craft include path and lib path. Finally, you can build what you want with libvlc dependency.
Conclusion
Up to now, KDE Connect is using QtMultimedia rather than phonon and phonon-vlc to play a sound. But this work could be also useful for other applications or libraries who depend on phonon, phonon-vlc or vlc. This small step may help build them successfully on macOS.
I’m Weixuan XIAO, with the nickname: Inoki, sometimes Inokinoki is used to avoid duplicated username.
I’m glad to be selected in Google Summer of Code 2019 to work for KDE Community to make KDE Connect work on macOS. And I’m willing to be a long-term contributor in KDE Community.
As a Chinese student, I’m studying in France for my engineering degree. At the same time, I’m waiting for my bachelor degree at Shanghai University.
I major in Real-Time System and Embedded Engineering. With strong interests in Operating System and Computer Architecture, I like playing with small devices like Arduino and Raspberry Pi, different systems like macOS and Linux(especially Manjaro with KDE, they are the best partner).
Japanese culture makes me crazy, for example, the animation and the game. Even my nickname is actually the pronunciation of my real name in Japanese. So if all of these is the choice of Steins Gate, I’ll normally accept them :)
I speak Chinese, French, English, and a little Japanese. But I realize that my English is awful. So if I make any mistake, please tell me. This would improve my English and I will appreciate it.
I hope we can have a good summer in 2019. And have some good codes :)