Add a script to create a universal app package
authorFelix Weilbach <felix.weilbach@nextcloud.com>
Fri, 5 Nov 2021 12:52:54 +0000 (13:52 +0100)
committerMatthieu Gallien (Rebase PR Action) <matthieu_gallien@yahoo.fr>
Wed, 10 Nov 2021 13:57:47 +0000 (13:57 +0000)
Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
admin/osx/make_universal.py [new file with mode: 0755]

diff --git a/admin/osx/make_universal.py b/admin/osx/make_universal.py
new file mode 100755 (executable)
index 0000000..abc6e67
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+import sys
+import os
+import subprocess
+
+
+# A general note: We first produce a x86_64 and a arm64 app package
+# and then merge them together instead of compiling the desktop client
+# with the CMake option CMAKE_OSX_ARCHITECTURES="x86_64;arm64" because
+# macdeployqt can not handle universal binaries well. In the future
+# with Qt6 this might change and this script will become obsolete.
+
+
+def usage(program_name):
+    print("Creates a universal app package from a x86_64 and a arm64 app package.")
+    print("Usage: {} x86_64_app_file arm64_app_file output_directory".format(program_name))
+    print("Example: {} some_dir/Nextcloud.app some_other_dir/Nextcloud.app output_dir".format(program_name))
+
+
+def execute(command):
+    return subprocess.check_output(command)
+
+
+def path_relative_to_package(app_package_file_path, file_path):
+    if file_path.startswith(app_package_file_path):
+        relative_path = file_path[len(app_package_file_path):]
+        if relative_path.startswith("/"):
+            return relative_path[1:]
+        return relative_path
+    return file_path
+
+
+def is_executable(file_path):
+    output = str(execute(["file", file_path]))
+    if (("Mach-O 64-bit dynamically linked shared library" in output)
+        or ("Mach-O 64-bit executable" in output)):
+        return True
+    return False
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 4:
+        usage(sys.argv[0])
+        sys.exit(1)
+
+    x86_64_app_file = sys.argv[1]
+    if not os.path.exists(x86_64_app_file):
+        print("Can't create universal: Path {} already exists".format(x86_64_app_file))
+        sys.exit(1)
+    arm64_app_file = sys.argv[2]
+    if not os.path.exists(arm64_app_file):
+        print("Can't create universal: Path {} already exists".format(arm64_app_file))
+        sys.exit(1)
+    output_dir = sys.argv[3]
+
+    # Copy the Arm64 variant to the output location if possible
+    if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+    app_file_name = os.path.basename(arm64_app_file)
+    universal_app_file = os.path.join(output_dir, app_file_name)
+    if os.path.exists(universal_app_file):
+        print("Can't create universal: Path {} already exists".format(universal_app_file))
+        sys.exit(1)
+
+    execute(["cp", "-a", arm64_app_file, output_dir])
+
+    # Now walk through the copied arm64 version and replace the binaries
+    for root, dirs, files in os.walk(universal_app_file):
+        for f in files:
+            absoulte_file_path = os.path.join(root, f)
+            root_relative = path_relative_to_package(universal_app_file, root)
+            x86_64_absolute_path = os.path.join(x86_64_app_file, root_relative, f)
+            arm64_absolute_path = os.path.join(arm64_app_file, root_relative, f)
+            if os.path.islink(absoulte_file_path) or not is_executable(absoulte_file_path):
+                continue
+            try:
+                execute(["lipo", "-create", "-output", absoulte_file_path, arm64_absolute_path, x86_64_absolute_path])
+            except:
+                print("Could not merge {} with {} into {}!".format(arm64_absolute_path, x86_64_absolute_path, absoulte_file_path))
+
+    print("Finished :)")