diff --git a/docs/wayland-port.md b/docs/wayland-port.md index 8a8ad5135..b3a644798 100644 --- a/docs/wayland-port.md +++ b/docs/wayland-port.md @@ -2702,3 +2702,46 @@ in the check, because the check applied it to the same already-patched pin. Regenerate against a pristine pin: an older cider-src store path from before the patch existed, or reverse the committed patch first. And the verification has to apply the WHOLE SERIES to a pristine tree, which is what caught it. + +## ITERM2: WHAT IT WOULD TAKE, MEASURED RATHER THAN GUESSED + +The second north star, tried for the first time. It does not start. The interesting part is not that, +it is knowing exactly why without discovering it one dyld error at a time. + + dyld: Library not loaded: /System/Library/Frameworks/CryptoKit.framework/... + +scripts/macho-needs.py reads the load commands of a Mach-O and answers the whole question at once, +which is the difference between one round trip and thirty. For iTerm2 3.6.10, x86_64 slice: + + needs=89 present=63 in-bundle=10 missing=9 weak-missing=7 + +THE NINE, and they fall into exactly two groups: + + /System/Library/Frameworks/CryptoKit not in this tree, and not in Darling either + /System/Library/Frameworks/QuickLookUI + /System/Library/Frameworks/ScreenCaptureKit + /System/Library/Frameworks/SwiftUI + + /usr/lib/swift/libswift_Concurrency Swift 5.5 and later + /usr/lib/swift/libswiftSystem swift-system, 5.6 and later + /usr/lib/swift/libswiftSystem_Foundation + /usr/lib/swift/libswiftUniformTypeIdentifiers + /usr/lib/swift/libswiftWebKit + +THE SECOND GROUP IS ONE PIN BUMP. vendor/pins/swift is version.txt 5.2.2, and its build.sh extracts +the dylibs straight out of an official swift.org release package. Every missing library there +belongs to a LATER Swift, so moving that pin forward should supply all five at once, with no new +code. That is a tractable next step and it should be taken before anything else here. + +THE FIRST GROUP IS REAL WORK. SwiftUI in particular is not a stub anyone writes in an afternoon, and +a framework that loads while exporting nothing does not help: a two level namespace binds classes +and data at load time, so it fails at the first symbol the application really uses. + +AND A LOCAL TRAP WORTH KNOWING. The forty four swift dylibs in the runtime tree are 131 byte GIT LFS +POINTER FILES here and real Mach-O only in the nix pin. Anything asking whether the file EXISTS +reports a complete Swift runtime; dyld disagrees. macho-needs.py reads the magic for that reason and +reported not-macho=23 before the real ones were copied in, 0 after. + +The version of iTerm2 is worth stating too: the nixpkgs recipe points at the stable URL with a hash +that no longer matches what that URL serves, so the bundle here was fetched with its real hash. The +version inside it is still 3.6.10, the same as nixpkgs claims. diff --git a/scripts/macho-needs.py b/scripts/macho-needs.py new file mode 100644 index 000000000..250ce8d18 --- /dev/null +++ b/scripts/macho-needs.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""What a Mach-O binary asks the loader for, and which of those this prefix has. + +ONE ERROR AT A TIME IS THE SLOW WAY. A bundle that links forty frameworks fails on the first one +missing, gets that one added, and fails on the next; each round is a build and a run. The load +commands list every dependency up front, so the whole gap can be seen at once and the work ordered +by what actually matters. + +Handles FAT binaries by walking every architecture and reporting the x86_64 slice, which is the one +this port runs. Follows nothing: a dependency of a dependency is a separate question, and asking it +here would hide which of them the application itself named. + + scripts/macho-needs.py [prefix] [runtime] +""" +import struct +import sys + +FAT_MAGIC = 0xCAFEBABE +FAT_CIGAM = 0xBEBAFECA +MH_MAGIC_64 = 0xFEEDFACF +MH_CIGAM_64 = 0xCFFAEDFE +LC_LOAD_DYLIB = 0x0C +LC_LOAD_WEAK_DYLIB = 0x80000018 +LC_REEXPORT_DYLIB = 0x8000001F +LC_LOAD_UPWARD_DYLIB = 0x80000023 +CPU_TYPE_X86_64 = 0x01000007 + + +def slices(data): + """(offset, cputype) for each architecture in the file, thin or fat.""" + magic = struct.unpack(">I", data[:4])[0] + if magic in (FAT_MAGIC, FAT_CIGAM): + count = struct.unpack(">I", data[4:8])[0] + out = [] + for i in range(count): + base = 8 + i * 20 + cputype, _sub, offset, _size, _align = struct.unpack(">iiIII", data[base:base + 20]) + out.append((offset, cputype)) + return out + return [(0, CPU_TYPE_X86_64)] + + +def dylibs(data, offset): + magic = struct.unpack(" 2 else "/tmp/cider-appkit-1000/prefix" + runtime = sys.argv[3] if len(sys.argv) > 3 else "/tmp/cider-appkit-1000/rt/libexec/cider" + + with open(path, "rb") as handle: + data = handle.read() + + wanted = [] + for offset, cputype in slices(data): + if cputype != CPU_TYPE_X86_64: + continue + wanted = dylibs(data, offset) + break + + import os + + # @rpath AND @executable_path ARE NOT PATHS, and treating them as if they were turns every + # framework an application ships INSIDE ITS OWN BUNDLE into a missing one. iTerm2 has ten of + # those, and a report that names them alongside SwiftUI is worse than no report: it buries the + # four that matter under six that are already there. + exe_dir = os.path.dirname(os.path.abspath(path)) + bundle_roots = [exe_dir, os.path.join(exe_dir, "..", "Frameworks")] + + def resolve(name): + if name.startswith("@executable_path/"): + return [os.path.normpath(os.path.join(exe_dir, name[len("@executable_path/"):]))] + if name.startswith("@loader_path/"): + return [os.path.normpath(os.path.join(exe_dir, name[len("@loader_path/"):]))] + if name.startswith("@rpath/"): + tail = name[len("@rpath/"):] + return [os.path.normpath(os.path.join(root, tail)) for root in bundle_roots] + # The guest sees a union of the prefix over the runtime tree, so either one counts. + return [root + name for root in (prefix, runtime)] + + def is_macho(candidate): + """EXISTING IS NOT THE SAME AS LOADABLE. + + The swift dylibs in this tree are 131 byte git-lfs POINTER FILES locally and real Mach-O + only in the nix pin, so a check that asks whether the path exists reports a runtime that is + entirely there and dyld disagrees. Read the magic.""" + try: + with open(candidate, "rb") as handle: + head = handle.read(4) + except OSError: + return False + return head in (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe", b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca") + + missing, weak_missing, present, bundled, notmacho = [], [], [], [], [] + for name, weak in wanted: + candidates = resolve(name) + found = any(os.path.exists(candidate) for candidate in candidates) + loadable = any(is_macho(candidate) for candidate in candidates) + if found and not loadable: + notmacho.append(name) + elif loadable: + (bundled if name.startswith("@") else present).append(name) + elif weak: + weak_missing.append(name) + else: + missing.append(name) + + print(f"needs={len(wanted)} present={len(present)} in-bundle={len(bundled)} " + f"missing={len(missing)} not-macho={len(notmacho)} weak-missing={len(weak_missing)}") + for name in notmacho: + print(f"NOTMACHO {name}") + for name in missing: + print(f"MISSING {name}") + for name in weak_missing: + print(f"weak {name}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())