From ebdc2f2f96dbbe73c3012e0332a7fed4c3520c86 Mon Sep 17 00:00:00 2001 From: Niclas Overby Date: Thu, 13 Aug 2026 22:56:10 +0200 Subject: [PATCH] tool(cider): a work list for what LINKS CLEANLY AND DOES NOTHING The missing symbol sweep in docs/libreoffice-gap.md answers a different question and is EXHAUSTED. Every wall since it was written has been invisible to it: a C function that exists, prints STUB and returns nil an ObjC method that exists and calls NSUnimplementedMethod a selector no class here implements at all None of those three is an undefined symbol, so comparing symbol tables cannot find any of them. backingScaleFactor and accessibilityDisplayShouldReduceMotion were both missing SELECTORS on classes that exist, and CTFontCopyTable was a present symbol that answered nil 782 times in one run. This reads the markers WE ALREADY WRITE in our own sources and intersects them with what the application actually references: its imported symbols, and the selector names in its __objc_methname section, which is exactly the set of selectors it can send. Against LibreOffice it reports 24 stubbed C functions it imports and 74 stubbed methods whose selector it uses, each with the file to edit. The third list, selectors nothing here defines, is a HEURISTIC and says so: an application also sends selectors to its own classes, so it over reports and is printed last for that reason. docs/libreoffice-worklist.txt is the current output, kept so the next session starts from a list rather than from a run. Also here: the colour space patch that the GUI needs. O2ColorSpaceCreateWithName handled only sRGB and Display P3 and returned NULL for everything else, which is not a degraded colour but a caller that cannot make a context. LibreOffice asks for GenericGrayGamma2_2 while bringing its GUI up and dies on it. The gamma is NOT modelled and the comment says so: each name maps to the device space with the right number of components, so a 2.2 gamma grey and a linear grey come out the same. That is a colour accuracy difference against a process that does not start. --- docs/libreoffice-worklist.txt | 165 +++++++++++++++++ scripts/lo-work-list.py | 172 ++++++++++++++++++ .../0015-colorspace-named-spaces.patch | 44 +++++ 3 files changed, 381 insertions(+) create mode 100644 docs/libreoffice-worklist.txt create mode 100644 scripts/lo-work-list.py create mode 100644 vendor/patches/cocotron/0015-colorspace-named-spaces.patch diff --git a/docs/libreoffice-worklist.txt b/docs/libreoffice-worklist.txt new file mode 100644 index 000000000..874da0c96 --- /dev/null +++ b/docs/libreoffice-worklist.txt @@ -0,0 +1,165 @@ + +== STUBBED C FUNCTIONS THE APPLICATION IMPORTS: 24 + CGContextShowGlyphsAtPositions [vendor/src/cocotron/CoreGraphics/CGContext.m] + CGDisplayScreenSize [vendor/src/cocotron/CoreGraphics/CGDirectDisplay.m] + CGFontCreateWithDataProvider [vendor/src/cocotron/CoreGraphics/CGFont.m] + CGImageSourceCreateWithDataProvider [src/darwin/frameworks/ImageIO/src/CGImageSource.m] + CTFontCopyAttribute [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyCharacterSet [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyFamilyName [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyFontDescriptor [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyGraphicsFont [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyLocalizedName [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyPostScriptName [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyVariation [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCopyVariationAxes [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCreateCopyWithAttributes [vendor/src/cocotron/CoreText/CTFont.m] + CTFontCreateCopyWithSymbolicTraits [vendor/src/cocotron/CoreText/CTFont.m] + CTFontDescriptorCreateWithAttributes [vendor/src/cocotron/CoreText/stubs.c] + CTFontDrawGlyphs [vendor/src/cocotron/CoreText/CTFont.m] + CTFontGetBoundingRectsForGlyphs [vendor/src/cocotron/CoreText/CTFont.m] + CTFontGetSymbolicTraits [vendor/src/cocotron/CoreText/CTFont.m] + CTFontGetUnitsPerEm [vendor/src/cocotron/CoreText/CTFont.m] + CTFontManagerCopyAvailableFontFamilyNames [vendor/src/cocotron/CoreText/CTFontManager.m] + CTFontManagerRegisterFontsForURL [vendor/src/cocotron/CoreText/CTFontManager.m] + NSAccessibilityUnignoredChildren [vendor/src/cocotron/AppKit/NSAccessibility.m] + NSSetFocusRingStyle [vendor/src/cocotron/AppKit/NSGraphics.m] + +== STUBBED METHODS WHOSE SELECTOR THE APPLICATION USES: 74 + CGColor [vendor/src/cocotron/AppKit/NSColor.subproj/NSColor.m] + acceptsFirstMouse: [vendor/src/cocotron/AppKit/NSView.m] + activateIgnoringOtherApps: [vendor/src/cocotron/AppKit/NSApplication.m] + appearanceNamed: [vendor/src/cocotron/AppKit/NSAppearance.m] + bitmapImageRepForCachingDisplayInRect: [vendor/src/cocotron/AppKit/NSView.m] + cacheDisplayInRect:toBitmapImageRep: [vendor/src/cocotron/AppKit/NSView.m] + cellSize [vendor/src/cocotron/AppKit/NSTextAttachmentCell.m] + changeCount [vendor/src/cocotron/AppKit/NSPasteboard.m] + characterIndexForPoint: [vendor/src/cocotron/AppKit/NSTextView.subproj/NSTextView.m] + colorUsingColorSpaceName:device: [vendor/src/cocotron/AppKit/NSColor.subproj/NSColor.m] + currentContext [vendor/src/cocotron/AppKit/NSAnimation/NSAnimationContext.m] + dataForType: [vendor/src/cocotron/AppKit/NSPasteboard.m] + declareTypes:owner: [vendor/src/cocotron/AppKit/NSPasteboard.m] + displayRectIgnoringOpacity:inContext: [vendor/src/cocotron/AppKit/NSView.m] + dragImage:at:offset:event:pasteboard:source:slideBack: [vendor/src/cocotron/AppKit/NSDraggingManager.m] + drawStatusBarBackgroundInRect:withHighlight: [vendor/src/cocotron/AppKit/NSStatusItem.m] + drawWithFrame:inView: [vendor/src/cocotron/AppKit/NSTextAttachmentCell.m] + drawableSize [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + duration [vendor/src/cocotron/AppKit/NSAnimation/NSAnimationContext.m] + font [vendor/src/cocotron/AppKit/NSText.m] + framebufferOnly [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + getRed:green:blue:alpha: [vendor/src/cocotron/AppKit/NSColor.subproj/NSColor.m] + hideOtherApplications: [vendor/src/cocotron/AppKit/NSApplication.m] + highlightWithLevel: [vendor/src/cocotron/AppKit/NSColor.subproj/NSColor_catalog.m] + isEnabled [vendor/src/cocotron/AppKit/NSFontManager.m] + isOpaque [vendor/src/cocotron/AppKit/NSSplitView.m] + isValid [vendor/src/cocotron/AppKit/NSImage.m] + maxSize [vendor/src/cocotron/AppKit/NSText.m] + menu [vendor/src/cocotron/AppKit/NSResponder.m] + minSize [vendor/src/cocotron/AppKit/NSText.m] + mouseEntered: [vendor/src/cocotron/AppKit/NSButtonCell.m] + mouseExited: [vendor/src/cocotron/AppKit/NSButtonCell.m] + mouseLocation [vendor/src/cocotron/AppKit/NSDisplay.m] + name [vendor/src/cocotron/AppKit/NSPasteboard.m] + nextDrawable [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + objectEnumerator [vendor/src/cocotron/CoreData/NSManagedObjectMutableSet.m] + pasteboardWithName: [vendor/src/cocotron/AppKit/NSDisplay.m] + pause [vendor/src/cocotron/AppKit/NSSound.m] + pixelFormat [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + play [vendor/src/cocotron/AppKit/NSSound.m] + reset [vendor/src/cocotron/CoreData/NSManagedObjectContext.m] + screens [vendor/src/cocotron/AppKit/NSDisplay.m] + selectedRange [vendor/src/cocotron/AppKit/NSText.m] + sendEvent: [vendor/src/cocotron/AppKit/NSWindow.m] + setAccessoryView: [vendor/src/cocotron/AppKit/NSColorPanel.m] + setAllowsOtherFileTypes: [vendor/src/cocotron/AppKit/NSSavePanel.subproj/NSSavePanel.m] + setBackgroundColor: [vendor/src/cocotron/AppKit/NSText.m] + setCanCreateDirectories: [vendor/src/cocotron/AppKit/NSSavePanel.subproj/NSSavePanel.m] + setColorspace: [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + setData:forType: [vendor/src/cocotron/AppKit/NSPasteboard.m] + setDelegate: [vendor/src/cocotron/AppKit/NSText.m] + setDepthLimit: [vendor/src/cocotron/AppKit/NSWindow.m] + setDevice: [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + setDisplaySyncEnabled: [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + setDrawableSize: [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + setDrawsBackground: [vendor/src/cocotron/AppKit/NSText.m] + setEditable: [vendor/src/cocotron/AppKit/NSText.m] + setFont: [vendor/src/cocotron/AppKit/NSText.m] + setImage: [vendor/src/cocotron/AppKit/NSBrowserCell.m] + setMaxSize: [vendor/src/cocotron/AppKit/NSText.m] + setMenu: [vendor/src/cocotron/AppKit/NSResponder.m] + setMinSize: [vendor/src/cocotron/AppKit/NSText.m] + setName: [vendor/src/cocotron/AppKit/NSSound.m] + setPixelFormat: [vendor/src/cocotron/QuartzCore/CAMetalLayer.mm] + setSelectable: [vendor/src/cocotron/AppKit/NSText.m] + sizeToFit [vendor/src/cocotron/AppKit/NSBox.m] + stop [vendor/src/cocotron/AppKit/NSSound.m] + string [vendor/src/cocotron/AppKit/NSText.m] + textColor [vendor/src/cocotron/AppKit/NSText.m] + type [vendor/src/cocotron/CoreData/NSPersistentStore.m] + types [vendor/src/cocotron/AppKit/NSPasteboard.m] + unhideAllApplications: [vendor/src/cocotron/AppKit/NSApplication.m] + userInfo [vendor/src/cocotron/CoreData/NSPropertyDescription.m] + windowNumbersWithOptions: [vendor/src/cocotron/AppKit/NSWindow.m] + +== SELECTORS THE APPLICATION USES THAT NOTHING HERE DEFINES: 486 (heuristic, includes the application's own) + absoluteString + accessibilityActionDescription: + accessibilityActionNames + accessibilityApplicationFocusedUIElement + accessibilityAttributeNames + accessibilityAttributeValue: + accessibilityAttributeValue:forParameter: + accessibilityAttributedStringForRange: + accessibilityChildren + accessibilityChildrenInNavigationOrder + accessibilityColumns + accessibilityContents + accessibilityFocusedUIElement + accessibilityFrame + accessibilityFrameForRange: + accessibilityHelp + accessibilityHitTest: + accessibilityHorizontalScrollBar + accessibilityIdentifier + accessibilityIsAttributeSettable: + accessibilityIsIgnored + accessibilityLabel + accessibilityLineForIndex: + accessibilityMaxValue + accessibilityMinValue + accessibilityNotifiesWhenDestroyed + accessibilityNumberOfCharacters + accessibilityOrientation + accessibilityParameterizedAttributeNames + accessibilityParent + accessibilityPerformAction: + accessibilityPerformDecrement + accessibilityPerformIncrement + accessibilityPerformPick + accessibilityPerformPress + accessibilityPerformShowMenu + accessibilityRTFForRange: + accessibilityRangeForIndex: + accessibilityRangeForLine: + accessibilityRangeForPosition: + accessibilityRole + accessibilityRoleDescription + accessibilityRows + accessibilitySelectedChildren + accessibilitySelectedText + accessibilitySelectedTextRange + accessibilityServesAsTitleForUIElements + accessibilitySetOverrideValue:forAttribute: + accessibilitySetValue:forAttribute: + accessibilitySharedCharacterRange + accessibilitySharedTextUIElements + accessibilityStringForRange: + accessibilityStyleRangeForIndex: + accessibilitySubrole + accessibilityTabs + accessibilityTitle + accessibilityTitleUIElement + accessibilityTopLevelUIElement + accessibilityValue + accessibilityVerticalScrollBar + ... and 426 more diff --git a/scripts/lo-work-list.py b/scripts/lo-work-list.py new file mode 100644 index 000000000..da0b63a0e --- /dev/null +++ b/scripts/lo-work-list.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""What LibreOffice needs that this fork has but does not MEAN, as one work list. + +The missing-symbol sweep (docs/libreoffice-gap.md) is a different question and is already +answered. It finds what the linker cannot resolve, and it is exhausted: the walls since then have +all been things that LINK CLEANLY AND DO NOTHING. + + a C function that exists and prints STUB and returns nil + an ObjC method that exists and calls NSUnimplementedMethod + a selector no class here implements at all + +None of those three is an undefined symbol, so none can be found by comparing symbol tables. This +finds them instead, by reading our own sources for the markers we already write, and intersecting +with what the application actually references. + +Usage: + scripts/lo-work-list.py ... +""" +import os +import re +import subprocess +import sys + +# A function body that contains one of these is present, linkable, and useless. +STUB_MARKERS = ("printf(\"STUB", "NSUnimplementedMethod(", "NSInvalidAbstractInvocation(") + +C_FUNC = re.compile( + r'^[A-Za-z_][A-Za-z0-9_ \t\*]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*\{', re.M) +OBJC_METHOD = re.compile(r'^[-+]\s*\(([^)]*)\)\s*([^{;]+)\{', re.M) + + +def body_of(text, brace_pos): + """The text between the brace at brace_pos and its match, or a bounded slice.""" + depth, i, n = 0, brace_pos, len(text) + while i < n: + if text[i] == '{': + depth += 1 + elif text[i] == '}': + depth -= 1 + if depth == 0: + return text[brace_pos:i] + i += 1 + return text[brace_pos:brace_pos + 4000] + + +def selector_of(decl): + """The selector from an ObjC method declaration, e.g. 'setFoo: (int) x bar: (int) y'.""" + parts = re.findall(r'([A-Za-z_][A-Za-z0-9_]*:)', decl) + if parts: + return "".join(parts) + m = re.match(r'\s*([A-Za-z_][A-Za-z0-9_]*)\s*$', decl.strip()) + return m.group(1) if m else None + + +def scan_sources(roots): + """(stub_c_functions, stub_selectors, all_defined_selectors)""" + stub_funcs, stub_sels, all_sels = {}, {}, set() + for root in roots: + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in ("buck-out", ".git", ".jj", "submodules")] + for name in filenames: + if not name.endswith((".m", ".c", ".mm")): + continue + path = os.path.join(dirpath, name) + try: + text = open(path, errors="ignore").read() + except OSError: + continue + for m in OBJC_METHOD.finditer(text): + sel = selector_of(m.group(2)) + if not sel: + continue + all_sels.add(sel) + body = body_of(text, m.end() - 1) + if any(k in body for k in STUB_MARKERS): + stub_sels.setdefault(sel, path) + for m in C_FUNC.finditer(text): + fn = m.group(1) + if fn in ("if", "for", "while", "switch", "return", "sizeof"): + continue + body = body_of(text, m.end() - 1) + if any(k in body for k in STUB_MARKERS): + stub_funcs.setdefault(fn, path) + return stub_funcs, stub_sels, all_sels + + +def app_machos(bundle): + out = [] + for dirpath, _dirnames, filenames in os.walk(bundle): + for name in filenames: + p = os.path.join(dirpath, name) + if os.path.islink(p) or not os.path.isfile(p): + continue + try: + with open(p, "rb") as f: + if f.read(4) not in (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe"): + continue + except OSError: + continue + out.append(p) + return out + + +def app_imports_and_selectors(bundle): + imports, selectors = set(), set() + for path in app_machos(bundle): + try: + r = subprocess.run(["llvm-nm", "-u", path], capture_output=True, text=True, timeout=120) + for line in r.stdout.splitlines(): + s = line.strip() + if s.startswith("U "): + s = s[2:].strip() + if s.startswith("_"): + imports.add(s[1:]) + except Exception: + pass + # Selector names live in their own cstring section, which is exactly the set of + # selectors this binary can send. + try: + r = subprocess.run( + ["llvm-objdump", "--macho", "--section", "__TEXT,__objc_methname", path], + capture_output=True, text=True, timeout=180) + for line in r.stdout.splitlines(): + m = re.match(r'^[0-9a-f]+\s+(.+)$', line.strip()) + if m: + selectors.add(m.group(1).strip()) + except Exception: + pass + return imports, selectors + + +def main(): + if len(sys.argv) < 3: + raise SystemExit(__doc__) + bundle, roots = sys.argv[1], sys.argv[2:] + + print("reading the application...", file=sys.stderr) + imports, selectors = app_imports_and_selectors(bundle) + print(f" {len(imports)} imported symbols, {len(selectors)} selector names", file=sys.stderr) + + print("reading our sources...", file=sys.stderr) + stub_funcs, stub_sels, all_sels = scan_sources(roots) + print(f" {len(stub_funcs)} stubbed C functions, {len(stub_sels)} stubbed methods, " + f"{len(all_sels)} methods defined", file=sys.stderr) + + wanted_funcs = sorted(f for f in stub_funcs if f in imports) + wanted_sels = sorted(s for s in stub_sels if s in selectors) + # A selector the application can send that NOTHING here defines. Heuristic by nature: the + # application also sends selectors to its own classes, so this over-reports and is ranked + # last for that reason. + absent = sorted(s for s in selectors + if s not in all_sels and re.match(r'^[a-z][A-Za-z0-9_:]*$', s) + and (s.startswith("accessibility") or s.startswith("set") or ":" not in s)) + + print(f"\n== STUBBED C FUNCTIONS THE APPLICATION IMPORTS: {len(wanted_funcs)}") + for f in wanted_funcs: + print(f" {f} [{stub_funcs[f]}]") + + print(f"\n== STUBBED METHODS WHOSE SELECTOR THE APPLICATION USES: {len(wanted_sels)}") + for s in wanted_sels: + print(f" {s} [{stub_sels[s]}]") + + print(f"\n== SELECTORS THE APPLICATION USES THAT NOTHING HERE DEFINES: {len(absent)}" + f" (heuristic, includes the application's own)") + for s in absent[:60]: + print(f" {s}") + if len(absent) > 60: + print(f" ... and {len(absent) - 60} more") + + +if __name__ == "__main__": + main() diff --git a/vendor/patches/cocotron/0015-colorspace-named-spaces.patch b/vendor/patches/cocotron/0015-colorspace-named-spaces.patch new file mode 100644 index 000000000..612a9ae2f --- /dev/null +++ b/vendor/patches/cocotron/0015-colorspace-named-spaces.patch @@ -0,0 +1,44 @@ +diff -u --recursive a/Onyx2D/O2ColorSpace.m b/Onyx2D/O2ColorSpace.m +--- a/Onyx2D/O2ColorSpace.m 2026-08-13 22:12:11.800560362 +0200 ++++ b/Onyx2D/O2ColorSpace.m 2026-08-13 22:12:11.814382827 +0200 +@@ -133,7 +133,6 @@ + } + + O2ColorSpaceRef O2ColorSpaceCreateWithName(CFStringRef name) { +- printf("CALLED: O2ColorSpaceCreateWithName\n"); + if (CFStringCompare(name, kO2ColorSpaceSRGB, 0) == kCFCompareEqualTo) { + O2ColorSpaceRef cs = [[O2ColorSpace alloc] initWithDeviceRGB]; + cs->_name = kO2ColorSpaceSRGB; +@@ -144,6 +143,32 @@ + cs->_type = kO2ColorSpaceModelRGB; + return cs; + } ++ /* ++ * THE REST OF THE NAMED SPACES. Only sRGB and Display P3 were handled, and everything else ++ * returned NULL, which is not a degraded colour: a caller that asks for a colour space and ++ * gets nothing cannot make a context, and the failure surfaces somewhere unrelated. ++ * LibreOffice asks for GenericGrayGamma2_2 while bringing its GUI up and died on it, with ++ * "unknown color space name" being the only clue and appearing two lines earlier. ++ * ++ * THE GAMMA IS NOT MODELLED, and that is the honest limitation to write down rather than ++ * hide: this maps each name to the device space with the right NUMBER OF COMPONENTS, so a ++ * grey stays grey and an RGB stays RGB, but a 2.2 gamma grey and a linear grey come out the ++ * same. That is a colour accuracy difference, visible only in a side by side comparison, ++ * where the alternative was a process that does not start. ++ */ ++ if (CFStringCompare(name, kO2ColorSpaceGenericGray, 0) == kCFCompareEqualTo || ++ CFStringCompare(name, kO2ColorSpaceGenericGrayGamma2_2, 0) == kCFCompareEqualTo) { ++ return [[O2ColorSpace alloc] initWithDeviceGray]; ++ } ++ if (CFStringCompare(name, kO2ColorSpaceGenericRGB, 0) == kCFCompareEqualTo || ++ CFStringCompare(name, kO2ColorSpaceGenericRGBLinear, 0) == kCFCompareEqualTo || ++ CFStringCompare(name, kO2ColorSpaceAdobeRGB1998, 0) == kCFCompareEqualTo) { ++ return [[O2ColorSpace alloc] initWithDeviceRGB]; ++ } ++ if (CFStringCompare(name, kO2ColorSpaceGenericCMYK, 0) == kCFCompareEqualTo) { ++ return [[O2ColorSpace alloc] initWithDeviceCMYK]; ++ } ++ + printf("unknown color space name\n"); + CFShow(name); + return NULL; -- 2.51.2