Cover photo

Reclaim wasted space on MacOS

Newer apps come with both Intel and ARM architectures which effectively doubles its size, sucking up storage space that can be put to better use.

Open your favourite Text editor (I use Textmate) and paste in the following code:

`import os import subprocess import sys import tempfile import time import argparse

LIPO = "/usr/bin/lipo" FILE = "/usr/bin/file" LDID = ""

def clean_bin(bin, arch): subprocess.check_output( [ LIPO, "-thin", arch, bin, "-output", bin + f".{arch}", ] ) os.remove(bin) os.rename(bin + f".{arch}", bin)

def sign_bin(bin, no_ent): entitlements = "" try: entitlements = ( subprocess.check_output([LDID, "-e", bin], stderr=sys.stderr) .decode() .strip() ) except subprocess.CalledProcessError: pass

if not entitlements or no_ent:
    status = subprocess.call([LDID, "-S", bin])
    if status != 0:
        print("Failed to sign %s" % bin)
    return

fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, "w") as f:
    f.write(entitlements)

status = subprocess.call([LDID, f"-S{tmp_file}", bin])
os.close(fd)
os.remove(tmp_file)

if status != 0:
    print("Failed to sign %s" % bin)

def duplicate_app(app_dir, output_dir): if not os.path.exists(output_dir): print("Output dir does not exist: %s" % output_dir) return

os.makedirs(os.path.join(output_dir, os.path.split(app_dir)[-1]), exist_ok=True)
rsync_p = subprocess.Popen(
    [
        "rsync",
        "-r",
        "-v",
        "-aHz",
        f"{app_dir}",
        f"{output_dir}",
    ],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)

last = time.time()
lines = 0
total_time = 0
for line in iter(lambda: rsync_p.stdout.readline(), b""):
    lines += 1
    total_time += time.time() - last
    last = time.time()
    sys.stdout.write(
        "\r" + str(round(lines / total_time, 2)) + " files/sec; " + str(lines) + " "
    )

return os.path.join(output_dir, os.path.split(app_dir)[-1])

def is_mach(path): if os.path.islink(path): return False

if not os.access(path, os.X_OK):
    return False

output = subprocess.check_output([FILE, "--mime-type", path])
return output.split()[-1].decode() == "application/x-mach-binary"

def is_universal(path, target_arch): output = subprocess.check_output([LIPO, "-info", path]) output = output.decode().split(":")[-1].strip() archs = output.split()

if len(archs) == 1:
    return False

if target_arch in archs:
    return target_arch

if target_arch == "arm64":
    if "arm64e" in archs:
        return "arm64e"

if target_arch == "arm64e":
    if "arm64" in archs:
        return "arm64"

if target_arch != "i386" and "i386" in archs:
    if target_arch == "x86_64" and "x86_64" in archs:
        return "x86_64"

    if target_arch == "arm64e" or target_arch == "arm64":
        if "x86_64" in archs:
            return "x86_64"

        if "arm64e" in archs:
            return "arm64e"

        if "arm64" in archs:
            return "arm64"

return False

def main(): global LDID parser = argparse.ArgumentParser() parser.add_argument( "-app", "--app_dir", nargs="+", type=str, required=True, help="The app to armify", ) parser.add_argument( "-o", "--output_dir", type=str, default=os.getcwd(), help="Where the copy of the app is stored; defaults to working directory", ) parser.add_argument( "-arch", "--arch", type=str, default=os.uname().machine, help="The architecture to archify to; default: python's architecture", ) parser.add_argument( "-ld", "--ldid", type=str, help="The path to ldid for resigning the binaries" )

parser.add_argument(
    "-Ns",
    "--no_sign",
    help="Do not sign the binaries with ldid",
    action="store_true",
)

parser.add_argument(
    "-Ne",
    "--no_entitleemtns",
    help="Do not sign the binaries with original entitlements with ldid",
    action="store_true",
)

args = parser.parse_args()
app_dir = sorted(list(set(args.app_dir)))

for dir in app_dir:
    if not os.path.exists(dir):
        print("App dir does not exist: %s" % dir)
        return

    output_dir = args.output_dir
    target_arch = args.arch

    if os.path.exists("/usr/local/bin/ldid"):
        LDID = "/usr/local/bin/ldid"

    if args.ldid:
        if os.path.exists(args.ldid):
            LDID = args.ldid
        else:
            print("Specified ldid not found")
            if LDID:
                print("Using: %s" % LDID, "\n")
            else:
                print("No ldid found\n")

    print("\nCreating a copy at", output_dir, f"({dir.split('/')[-1]})")

    dir = duplicate_app(dir, output_dir)

    print("\nExtracting the target binaries")

    for root, dirs, files in os.walk(dir):
        for file in files:
            file = os.path.join(root, file)
            if is_mach(file):
                arch = is_universal(file, target_arch)
                if arch:
                    print("Cleaning %s" % file)
                    clean_bin(file, arch)
                    if LDID and not args.no_sign:
                        print("Signing %s" % file)
                        sign_bin(file, args.no_entitleemtns)

if name == "main": main() `

Save it as archify.py or whatever you fancy.

Launch Terminal and navigate to the folder that you saved the archify.py script and run it:

./python3 archify.py -app <drag in your .app here> -o <output folder for the reduced .app> -arch arm64

for Intel macs, simply change arm64 to x86_64.

Let the script do its thing (the bigger the app, the longer it’ll take).

Once done, your newly lipo-ed app should be almost half its original size and will be found in the output folder you specified earlier.

A good practice is to test if the new app works before overwriting the current one as some apps will break, majority won’t.

That’s it!

Cover Photo by Pietro De Grandi on Unsplash