Welcome

Extension Resolver #

Dependency resolution for Godot extensions. Any addon that ships an extension.manifest.json at its root gets, for free: version-guarded dependency checking (not just “is it installed” but “is it installed at a version that actually satisfies what I need”), user-confirmed fetching of missing or out-of-range dependencies from their declared source, update-availability checking against already-installed extensions, and the unlock step for gated GDExtension binaries with a hard native-library dependency.

Available Free on GitHub

For Extension Authors #

  1. Ship extension.manifest.json at your addon’s root see the schema doc.
  2. Copy gate/extension_resolver_gate.gd verbatim into your own addons/<Name>/gate/ folder.
  3. In your EditorPlugin script: const Gate = preload("res://addons/<Name>/gate/extension_resolver_gate.gd"), then call Gate.ensure_unlocked(self, "<YourAddonId>", _on_unlocked) from _enter_tree() the gate script has no class_name (deliberately, so multiple addons’ copies don’t collide), so it’s always reached via preload(), same as this ecosystem’s existing gate convention. See the gate script’s own header comment for the exact contract.

That’s the entire integration surface everything else (version comparison, fetching, the Project Settings tab listing installed extensions and their update status) lives in Extension Resolver itself, not duplicated per-addon.

Extension Manifest Schema #

Extension Resolver for Godot reads one file per participating addon res://addons/<id>/extension.manifest.json to answer three questions: what version of this is installed, where can updates for it be found, and what does it depend on. Nothing else. This document defines that file’s shape. It is deliberately not Heathen-specific any Godot extension author can ship this file and get dependency resolution, version guarding, and update-checking for free, without adopting anything else Heathen-shaped.

File Location and Name

res://addons/<id>/extension.manifest.json, one per addon, sitting next to plugin.cfg. Filename deliberately avoids manifest.json (collides with the web/PWA manifest convention many tools already grep for) and avoids package.json (npm). extension.manifest.json is unambiguous and greppable.

📋
filename.js
{
  "schema_version": 2,
  "id": "FoundationGameFramework",
  "display_name": "Foundation for Game Framework",
  "version": "1.0.0",
  "repository_url": "https://github.com/heathen-engineering/Godot-Game-Framework",
  "documentation_url": "https://github.com/heathen-engineering/Godot-Game-Framework#readme",
  "support_url": "https://github.com/heathen-engineering/Godot-Game-Framework/issues",
  "license_url": "https://github.com/heathen-engineering/Godot-Game-Framework/blob/main/LICENSE",
  "publisher": {
    "name": "Heathen Engineering Limited",
    "url": "https://heathen.group"
  },
  "description": "The engine-agnostic Subsystem/World/GameMode runtime every other Foundation gem builds on. See [Discord](https://discord.gg/example) for support.",
  "source": {
    "type": "github_release",
    "repo": "heathen-engineering/Godot-Game-Framework"
  },
  "gated": true,
  "dependencies": [
    {
      "id": "FoundationXxHash",
      "min_version": "1.0.0",
      "source": {
        "type": "github_release",
        "repo": "heathen-engineering/Godot-xxHash"
      }
    }
  ]
}

Top-level fields

FieldRequiredNotes
schema_versionYesInteger. Lets the resolver evolve the format later without silently mis-parsing an old manifest. Starts at 1; 2 (current) only adds optional fields below, so a schema_version: 1 manifest with none of them is still perfectly valid — nothing was removed or repurposed.
idYesMust match the addon’s own folder name under res://addons/. This is the identity used everywhere else (dependency references, installed-version lookups) — not display_name, which is presentation-only and can change freely.
display_nameYesHuman-readable, shown in the resolver’s UI.
versionYesThe version of this addon, as currently installed. Semver (MAJOR.MINOR.PATCH), optional leading v stripped before comparison. This is the field the resolver diffs against a dependent’s min_version/max_version, and against source‘s latest-available version to decide whether an update exists.
repository_urlNoPlain human-facing link, shown in the UI (“View project”). Not used for fetching — that’s source‘s job. Optional because not every extension author wants to point at a public repo even if source resolves through one (e.g. private-release-gated extensions like Steamworks today).
documentation_urlNo (added in v2)Shown as a “Documentation” link in the settings tab’s standard-links row. Plain URL — the author’s own docs site, README, wiki, whatever’s appropriate.
support_urlNo (added in v2)Shown as a “Support” link — issue tracker, Discord, support form, whatever the publisher wants users to hit when something’s broken.
license_urlNo (added in v2)Shown as a “License” link — usually the repo’s own LICENSE file.
publisherNo (added in v2){ "name": string, "url"?: string }. Drives the settings tab’s UPM-style grouping — every installed addon is listed under a collapsible group named for its publisher, defaulted expanded. Addons with no publisher.name fall into one shared “Unknown Publisher” group rather than erroring. url (optional) is shown as the publisher name’s hyperlink if present, plain text otherwise.
descriptionNo (added in v2)A short blurb shown in the settings tab’s detail pane, in a self-expanding scrollable text block. May contain (url) Markdown-style links (e.g. to point at a Patreon/Sponsorship page) — the resolver converts just that link subset to BBCode for display; it is not a full Markdown renderer, don’t rely on other Markdown syntax rendering correctly.
sourceYes, unless the addon is never expected to be fetched/updated by the resolver (e.g. something a user is expected to always hand-install)Describes where and how to fetch a specific version of this extension. See “Source types” below.
gatedNo, default falsetrue if this addon ships a GDExtension binary with a hard native-library dependency and needs the inert-.gdextension.available → real-.gdextension unlock step (see “Gating convention” below) before Godot can load it. Pure-GDScript addons, or GDExtensions with no hard dependencies, leave this false/omitted — nothing to gate.
dependenciesNo, default []Array of dependency descriptors — see below.

Dependency descriptor

FieldRequiredNotes
idYesThe depended-on addon’s own id.
min_versionNoInclusive lower bound. Omitted = any installed version satisfies it (today’s HeathenDependencyManifest behavior — presence-only, no version check — stays the default, not a special case).
max_versionNoInclusive upper bound. Expected to be rare (used to pin against a known-incompatible future major version) — most dependencies should only ever need min_version.
sourceYesWhere to fetch this dependency from if it turns out to be missing. See “Source precedence” below for what happens when this disagrees with the dependency’s own self-declared source.

Source types

source.type is an open enum — the resolver dispatches on it, unrecognised values are reported as “can’t auto-fetch this one, install it manually” rather than a hard error (an extension can still declare metadata/dependencies even if its fetch mechanism isn’t one the resolver knows yet).

  • github_release (the only type implemented for v1): repo ("owner/name"), plus an optional asset_pattern. A GitHub Release is already scoped to a single tag/version, so the version doesn’t need to be baked into the asset filename the way {version}-templated patterns implied in an earlier draft — real build output is typically just a fixed name (FoundationXxHash.zip, not FoundationXxHash-1.0.0.zip). Asset selection, in order:
    • asset_pattern if present — supports an optional {version} placeholder for the (rarer) case where an author’s release process really does bake the version into the filename.
    • Otherwise, the release’s only asset, if it has exactly one.
    • Otherwise, the first asset whose name starts with id — the same fallback HeathenDependencyFetcher/heathen_gate.gd already use today. Fetching any tagged release (not only /releases/latest) is what makes targeting a specific version possible — same GitHub API, just parameterized by tag instead of hardcoded to latest.

Future candidates: a direct URL type, for extensions that don’t use GitHub Releases at all; something for a private/authenticated endpoint, for a not-yet-designed equivalent of Steamworks’ gated-SDK distribution model.

Expected zip layout

A release asset’s zip is extracted by locating the <id>/ path segment itself within each archive entry and keeping everything after it not by assuming a fixed nesting depth. This matters because it isn’t fixed in practice; no CI in this ecosystem tests the fetch/extract flow end-to-end. Searching for the id segment itself is robust without needing to standardize every extension author’s zip layout first.

Source precedence

An already-installed dependency’s own manifest is always authoritative for what to check against (its own version field) a dependent’s declared source for that dependency is only ever consulted when the dependency isn’t installed yet and something needs to fetch it. If two different installed extensions declare different source values for the same dependency id (e.g. two forks pointing at different repos), the resolver uses whichever was declared by the extension currently being resolved.

Gating convention

a gated addon ships its real GDExtension file as <id>.gdextension.available instead of <id>.gdextension, so Godot’s boot-time .gdextension auto-load scan never touches it while a hard dependency might still be missing. The resolver’s unlock step performed only after every entry in dependencies is confirmed present and version-satisfying renames the file to its real name and calls GDExtensionManager.load_extension() so it’s live in the current editor session without a restart.

Uninstall

The settings tab’s Remove button deletes res://addons/<id>/ and disables the addon’s plugin.cfg (if it has one), after a confirmation dialog. Before deleting, it scans every other installed manifest’s dependencies array for a reference to the addon being removed and lists any matches in the confirmation dialog as “will likely break” a soft warning, not a hard block; Remove still proceeds if the user confirms anyway.

Library Schema #

A Library is a catalogue of installable extensions a project doesn’t necessarily have installed yet as opposed to extension.manifest.json (see manifest-schema.md), which describes one already-installed addon authoritatively. A Library Entry is a summary a publisher writes about an extension, used purely for discovery/display; once an entry is actually installed, its own extension.manifest.json takes over completely and the Library Entry that pointed at it is never consulted again for anything functional (no fallback, ever this is a discovery tool, not a second source of truth).

Configured Libraries

res://addon_libraries.json

The list of which Libraries a project has added project-relative and meant to be checked into the project, so a whole team sees the same configured sources by default. Path is configurable via the extension_resolver/libraries_config_path Project Setting; defaults to res://addon_libraries.json if never set.

{
    "schema_version": 1,
    "libraries": [
        { "type": "url", "url": "https://raw.githubusercontent.com/.../library.json" },
        { "type": "local", "path": "C:/dev/my-library.json" }
    ]
}
  • url
    A direct link to one Library JSON file (a raw GitHub content URL, or any other plain HTTPS GET). Phase 1 does not scan a Git repo’s tree for multiple *.library.json files — that’s a Phase 2 concern (needs the same “list a repo’s tree” capability git_subfolder fetching does).
  • local
    A path to one Library JSON file on local disk.

Library JSON

the file a url/local source points at

{
    "schema_version": 1,
    "name": "Heathen Group — Godot Extensions",
    "publisher": { "name": "Heathen Group", "url": "https://heathen.group" },
    "entries": [
        {
            "id": "FoundationSteamworks",
            "display_name": "Foundation for Steamworks",
            "publisher": { "name": "Heathen Group", "url": "https://heathen.group" },
            "description": "...",
            "documentation_url": "https://heathen.group/kb/steam-welcome/",
            "license_url": "https://github.com/heathen-engineering/Godot-Foundation-for-Steamworks/blob/main/LICENSE",
            "support_url": "https://discord.gg/xmtRNkW7hW",
            "dependencies": [
                { "id": "FoundationGameFramework", "min_version": "1.0.0" }
            ],
            "source": {
                "type": "github_release",
                "repo": "heathen-engineering/Godot-Foundation-for-Steamworks"
            }
        }
    ]
}

Top-level fields

FieldRequiredNotes
schema_versionYesSame forward-compat convention as extension.manifest.json — newer than this resolver knows is a warning, not a hard failure.
nameNoThe Library’s own display name, shown in the management window’s list of configured Libraries.
publisherNo{ "name": string, "url"?: string } — the Library’s publisher, not necessarily every entry’s (an entry may declare its own publisher that differs, e.g. a curated third-party Library listing other authors’ work).
entriesYesArray of Library Entry descriptors, see below.

Library Entry fields

FieldRequiredNotes
idYesMust match what the real extension.manifest.json declares as its own id once installed — this is how the resolver recognizes an entry as already-installed (ExtensionManifestReader.read_manifest_for(id) != null).
display_nameNoShown in the Libraries tab; falls back to id if omitted.
publisherNoSame shape as the Library’s own publisher — drives the same UPM-style publisher-grouped tree the In Project tab already uses.
descriptionNoSame restricted (url)-only Markdown-to-BBCode handling as extension.manifest.json.
documentation_url / license_url / support_urlNoSame “standard links row” as extension.manifest.json.
dependenciesNo, default []Same shape as extension.manifest.json‘s dependency descriptors (id, min_version, max_version) — shown informationally in the detail pane; never affects anything once the entry is actually installed, since the installed manifest’s own dependencies takes over completely at that point.
sourceYesOnly github_release is supported in Phase 1 — see manifest-schema.md‘s “Source types” for the exact shape (repo, optional asset_pattern). Installing a not-yet-installed entry runs the identical fetch pipeline a missing-dependency fetch already uses.
access_urlNoReserved for Phase 2 (private-library detection) — a single link (e.g. GitHub Sponsors) shown when the entry is inaccessible with the current machine’s Git credentials. Harmless to include now; nothing reads it yet.

Per-machine cache

user://extension_resolver/library_cache/

Not user-configurable, not project-shared. One JSON file per configured source (keyed by a hash of its url/path), storing the last successfully-fetched Library JSON plus a fetch timestamp. Exists so the Libraries tab has something to show immediately on open, and so an explicit Refresh is the only thing that ever re-hits the network scanning is on-demand only, never a timer or file-system watch (this is a utility, not a background service).

Rate This Article!