Skip to content

Syntax highlighting silently fails (and Bundle.module crashes) in SwiftPM CLI-built app bundles #95

Description

@sohei56

Summary

When CodeEditLanguages is consumed by an app that is built and packaged
with swift build (plus a hand-rolled bundle-assembly step) rather than
Xcode
, two resource-resolution problems surface:

  1. Distributed app crashes on the first highlight lookup, because the
    SwiftPM-generated Bundle.module accessor cannot locate the resource
    bundle inside a .app and calls fatalError.
  2. Even with the bundle located, every tree-sitter query file fails to
    load
    because CodeLanguage.queryURL(for:) hardcodes a path layout that
    only matches the Xcode-produced bundle. The editor renders as plain gray
    text with no error.

Both stem from the same assumption — that the package is always built and
embedded by Xcode. Dev builds on the build machine work, which is why this is
easy to miss.

Environment

  • Package: CodeEditLanguages 0.1.20 (pulled transitively via
    CodeEditSourceEditor 0.15.2).
  • Build: swift build from the command line. No .xcodeproj. The app is
    assembled into a .app by a custom script that copies the SwiftPM resource
    bundle (CodeEditLanguages_CodeEditLanguages.bundle) into
    MyApp.app/Contents/Resources/.
  • macOS, Apple Silicon, current Swift toolchain.

Defect 1 — Bundle.module fatalErrors in a distributed .app

Root cause

The SwiftPM-generated accessor
(.build/…/CodeEditLanguages.build/DerivedSources/resource_bundle_accessor.swift)
probes exactly two locations:

let mainPath = Bundle.main.bundleURL.appendingPathComponent("CodeEditLanguages_CodeEditLanguages.bundle").path
let buildPath = "/Users/<build-machine>/…/.build/arm64-apple-macosx/debug/CodeEditLanguages_CodeEditLanguages.bundle"

let preferredBundle = Bundle(path: mainPath)
guard let bundle = preferredBundle ?? Bundle(path: buildPath) else {
    Swift.fatalError("could not load resource bundle: from \(mainPath) or \(buildPath)")
}
  • mainPath uses Bundle.main.**bundleURL** — i.e. the .app wrapper root,
    not Contents/Resources. A correctly-formed .app keeps resources in
    Contents/Resources, so this candidate never matches.
  • buildPath is a hardcoded absolute path on the build machine. It
    resolves during local development, but obviously not on any other machine.

Result on an end user's machine: neither candidate exists →
Swift.fatalError → the app crashes the first time a CodeLanguage's
resourceURL / query is accessed.

Bundle.module (CodeLanguage.resourceURL) is used here:

// CodeLanguage.swift
internal var resourceURL: URL? = Bundle.module.resourceURL

Repro

  1. Depend on CodeEditLanguages (directly or via CodeEditSourceEditor).
  2. swift build (no Xcode project).
  3. Assemble a .app and copy CodeEditLanguages_CodeEditLanguages.bundle
    into Contents/Resources/.
  4. Run the .app. First access to a language's queryURL → crash at the
    accessor's fatalError.
  • Expected: the bundle is found under Contents/Resources; no crash.
  • Actual: fatalError("could not load resource bundle …").

Defect 2 — query path assumes the Xcode bundle layout

Root cause

// CodeLanguage.swift
internal func queryURL(for highlights: String = "highlights") -> URL? {
    return resourceURL?
        .appendingPathComponent("Resources/tree-sitter-\(tsName)/\(highlights).scm")
}

The "Resources/" segment is unconditionally prepended. That matches the
directory nesting Xcode produces, but under the SwiftPM command-line build the
grammar files do not live at that relative path from the resolved
resourceURL, so queryURL points at a file that does not exist. The lookup
returns a URL that never resolves, no error is thrown, and highlighting
silently degrades to plain text.

Repro

  1. With Defect 1 worked around (bundle located), open any source file in an
    editor backed by CodeEditSourceEditor.
  2. Observe: no syntax colors; text is uniform gray.
  3. Log CodeLanguage.swift.queryURL and FileManager.fileExists on the
    returned path — the file is not there.
  • Expected: the .scm query loads; tokens are highlighted.
  • Actual: queryURL resolves to a non-existent path; queries silently
    fail.

Proposed fix

Both are small and, importantly, change nothing for Xcode consumers — the
new code paths only engage when the current lookup fails.

  1. Bundle resolution — before falling back to Bundle.module, probe
    Bundle.main.resourceURL (i.e. Contents/Resources), where a
    CLI-assembled app copies the SwiftPM resource bundle:

    private static let queryBundleResourceURL: URL? = {
        if let bundled = Bundle.main.resourceURL?
            .appendingPathComponent("CodeEditLanguages_CodeEditLanguages.bundle"),
           let bundle = Bundle(url: bundled) {
            return bundle.resourceURL
        }
        return Bundle.module.resourceURL
    }()
    
    internal var resourceURL: URL? = CodeLanguage.queryBundleResourceURL
  2. Layout-aware query path — resolve against whichever layout exists on
    disk, probing the nested (Resources/…) layout first so Xcode behavior is
    byte-for-byte unchanged, then the flat layout:

    guard let base = resourceURL else { return nil }
    let relativePath = "tree-sitter-\(tsName)/\(highlights).scm"
    let nested = base.appendingPathComponent("Resources/\(relativePath)")
    if FileManager.default.fileExists(atPath: nested.path) {
        return nested
    }
    return base.appendingPathComponent(relativePath)

Offer to contribute

We are already running these two changes as a local patch against 0.1.20 and
they resolve both issues with no observed regression for Xcode-built consumers.
Per the CodeEdit contribution convention (open an issue first, ask to be
assigned), we'd be glad to submit the PR — could you assign this to us? The
patch passes SwiftLint and keeps the existing code paths as the primary
lookup. Happy to adjust the approach if you'd prefer a different shape.

Thanks for maintaining CodeEditLanguages.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions