Extensions

What is an Extension?

An extension is, in it’s simplest form, just a folder with a config file (extension.toml). The Extension system will find that extension and if it’s enabled it will do whatever the config file tells it to do, which may include loading python modules, Carbonite plugins, shared libraries, applying settings etc.

There are many variations, you can have an extension whose sole purpose is just to enable 10 other extensions for example, or just apply some settings.

Extension in a single folder

Everything an extension has should be in or nested inside it’s root folder. This is a convention we are trying to adhere to. Looking at other package managers, like those in the Linux ecosystem, they often spread the content of packages across the filesystem, which makes some things easier (like loading shared libraries), but also creates problems.

Following this convention makes the installation step very simple - we just have to unpack a single folder

A typical extension looks like this:

[extensions-folder]
└── omni.appwindow
    │──bin
    │  └───windows-x86_64
    │      └───debug
    │          └─── omni.appwindow.plugin.dll
    │───config
    │   └─── extension.toml
    └───omni
        └───appwindow
            │─── _appwindow.cp36-win_amd64.pyd
            └─── __init__.py

This example contains a Carbonite plugin and a python module (which contains bindings to this plugin).

Single File Extensions

Single file Extensions are supported - i.e. Extensions consisting only of a config file, without any code. In this case the name of the config will be used as the extension ID. This is used to make a top-level extensions which we call an app. They are used as an application entry point, to unroll the whole extension tree.

The file extension can be anything (.toml for instance), but the recommendation is to name them with the .kit file extension, so that it can be associated with the kit.exe executable and launched with a single click.

[extensions-folder]
└── omni.exp.hello.kit

App Extensions

When .kit files are passed to kit.exe they are treated specially:

This: > kit.exe C:/abc/omni.exp.hello.kit

Is the same as: > kit.exe --empty --ext-path C:/abc/omni.exp.hello.kit --enable omni.exp.hello

It adds this .kit file as an extension and enables it, ignoring any default app configs, and effectively starting an app.

Single file (.kit file) extensions are considered apps, and “launch” button is added to it in the UI of the extension browser. For regular extension specify keyword: package.app = true in config file to mark your extension as an app, one users can launch from.

App extensions can be published, versioned etc. just like normal extensions. So for instance if the omni.exp.hello from above is published, we can just run Kit as:

> kit.exe omni.exp.hello.kit

Kit will pull it from the registry and start.

Extension Search Paths

Extensions are automatically searched for in specified folders.

Core Kit config kit-core.json specifies default search folders in /app/exts/foldersCore setting. This way Kit can find core extensions, it also looks for extensions in system specific documents folder for user convenience.

To add more folders to search paths there are a few ways:

  1. Pass --ext-folder [PATH] CLI param to kit.

  2. Add to array in settings: /app/exts/folders

  3. Use the omni::ext::ExtensionManager::addPath API to add more folders (also available in python).

To specify direct path to a specific extension use /app/exts/paths setting or the --ext-path [PATH] CLI parameter.

Example of adding extension seach path in a kit file:

[settings.app.exts]
folders.'++' = [
    "C:/hello/my_extensions"
]

Custom Search Paths Protocols

Both folders and direct paths can be extended to support other url schemes. If no scheme specified they are assumed to be local filesystem. Extension system provides API to implement custom protocol. This way an extension can be written to enable searching for extensions in different locations, for example: git repos.

E.g. --ext-folder foo://abc/def – extension manager will redirect that search paths to implementor of foo scheme, if it was registered.

Git URL as Extension Search Paths

Extension omni.kit.extpath.git implemennts following extension search path schemes: git, git+http, git+https, git+ssh. Optional URL query params are supported:

  • dir subdirectory of a git repo to use as a search paths (or direct path).

  • branch git branch to checkout

  • tag git tag to checkout

  • sha git sha to checkout

Example of usage with cmd arg:

> --ext-folder git://github.com/bob/somerepo.git?branch=main&dir=exts – Add exts subfolder and main branch of this git repo as extension search paths.

Example of usage in kit file:

[settings.app.exts]
folders.'++' = [
    "git+https://gitlab-master.nvidia.com/bob/some-repo.git?dir=exts&branch=feature"
]

After first checkout git path is cached into global cache. To pull updates:

  • use extension manager properties pages

  • setting: --/exts/omni.kit.extpath.git/autoUpdate=1

  • API call omni.kit.extpath.git.update_all_git_paths()

The Extension system automatically enables this extension if path with a scheme is added. It enables extensions specified in a setting: app/extensions/pathProtocolExtensions, which by default is ["omni.kit.extpath.git"].

Note

Git installation is required for this functionality. It expects git cmd to be available in system shell.

Extension Discovery

The Extension system monitors specified extension search folders (or direct paths) for changes. It automatically syncs all changed/added/removed extensions. Any subfolder which contains an extension.toml in the root or config folder is considered to be an extension.

The subfolder name uniquely identifies the extension and is used to extract the extension name and tag: [ext_name]-[ext_tag]

[extensions-folder]
└── omni.kit.example-gpu-2.0.1-stable.3+cp36
├── extension.toml
└── …

In this example we have an extension omni.kit.example-gpu-2.0.1-stable.3+cp36, where:

  • name: omni.kit.example

  • tag: gpu (optional, default is “”)

The version and other information (like supported platforms) are queried in the extension config file (see below). They may also be included in the folder name, which is what the system does with packages downloaded from a remote registry, but that is not “the ground truth” for that data. So in this example anything could have been after the “gpu” tag, e.g. omni.kit.example-gpu-whatever.

Extension Dependencies

When a Kit-based application starts it discovers all extensions and does nothing with them until some of the extensions are enabled. They can be enabled via config file or API. Each extension can depend on other extensions and this is where the whole application tree can unroll. The user may enable a high level extension like omni.usd_viewer which will bring in dozens of others.

An extension can express dependencies on other extensions using name, version and optionally tag. It is important to keep extensions versioned properly and express breaking changes using Semantic Versioning.

This is a good place to grasp what tag is for. If say extension foo depends on bar you may implement other versions of bar, like: bar-light, bar-prototype. If they still keep the contract the same (public API, expected behavior etc.) you can safely substitute bar without foo noticing. In other words if the extension is an interface, tag is the implementation.

The effect is that just enabling some high level extensions like omni.kit.window.script_editor will expand the whole dependency tree in the correct order without the user having to specify all of them or worry about initialization order.

One can also substitute extensions in a tree with the different version or tag, running the same end user experience, but with a different low-level building block.

When an extension is enabled the manager tries to satisfy all of it’s dependencies by recursively solving the dependency graph. This is a difficulty problem - If dependency resolution succeeds the whole dependency tree is enabled in order so that all dependents are enabled first. The opposite is true for disabling of extensions. All extensions who depend on our extension are disabled first. More details on the dependency system can be found in the C++ unit tests: source/tests/test.unit/ext/TestExtensions.cpp

A Dependency graph defines the order in which extensions are loaded - it is sorted topologically. There are however, many ways to sort the same graph (think of independent branches). To give finer control over startup order, the order parameters can be used. Each extension can use core.order config parameter to define it’s order and the order of dependencies can also be overriden with order param in [[dependencies]] section. Those with lower order will start as soon as possible. If there are multiple extensions that depend on one extension and are trying to override this order then the one that is loaded last will be used (according to dependency tree). In summary - the dependency order is always satisfied (or extensions won’t be started at all, if the graph contains cycles) and soft ordering is applied on top using config params.

Extension configuration file (extension.toml)

An Extension config file can specify:

  1. Dependencies to import

  2. Settings

  3. A variety of metadata/information which are used by the Extension Registry browser (for example)

TOML is the format used . See this this short tutorial. . Note in particular [[]] TOML syntax for array of objects and quotes around keys which contain special symbols(e.g. "omni.physx").

The config file should be placed in the root of the extension folder or in a config subfolder.

Note

All relative paths in config are relative to the extension folder. All paths accept tokens. (like ${platform}, ${config}, ${kit} etc). More info: Tokens.

There are no mandatory fields in a config, so even with empty config extension will be considered valid and can be enabled - without any effect.

Next we will list all the config fields the extension system uses, a config file may contain more. The Extension system provides a mechanism to query config files and hook into itself. That allows us to extend the extension system itself and add new config sections. For instance omni.kit.pipapi allows extensions to specify pip packages to be installed before enabling them. More info on that: Hooks. That also means that typos or unknown config settings will be left as is and no warning will be issued.

Config Fields

[core] section

For generic extension properties, used directly by the Extension Manager core system.

[core.reloadable] (default: true)

Is the extension reloadable? The Extension system will monitor the extension’s files for changes and try to reload the extension when any of them change. If the extension is marked as non-reloadable all other extensions that depend on it are also non-reloadable.

[core.order] (default: 0)

When extensions are independent of each other they are enabled in an undefined order. An extension can be ordered to be before (negative) or after (positive) other extensions

[package] section

Contains information used for publishing extensions and displaying user facing details about the package.

[package.version] (default: "0.0.0")

Extension version. This setting is required to be able to publish extensions to the remote registry. The Semantic Versioning concept is baked into theextension system, so make sure to follow the basic rules:

  • Before you reach 1.0.0, anything goes, but if you make breaking changes, increment the minor version.

  • After 1.0.0, only make breaking changes when you increment the major version.

  • Incrementing the minor version implies a backward compatible change. Let’s say extensions bar depends on foo-1.2.0. That means that foo-1.3.0, foo-1.4.5, etc.. are also suitable and can be enabled by extension system.

  • Use version numbers with three numeric parts such as 1.0.0 rather than 1.0.

  • Prerelease labels can also be used like so: 1.3.4-stable or 1.3.4-rc1.test.1.

[package.title] default: ""

User facing package name, used for UI.

[package.description] default: ""

User facing package description, used for UI.

[package.category] (default: "")

Extension category, used for UI. One of:

  • animation

  • graph

  • rendering

  • audio

  • simulation

  • example

  • internal

  • other

[package.app] (default: false)

Extension is an App. Used for UI to mark extension as an app. It adds “Launch” button to run kit with only this extesnion enabled. For single file extensions (.kit files it defaults to true).

[package.feature] (default: false)

Extension is a Feature. Used for UI, to show user facing extensions, suited to be enabled by user from UI. By default app can choose to show only those extensions in the list.

[package.toggleable] (default: true)

Indicates whether an extension can be toggled (i.e enabled/disabled) in the UI by user. There is another related setting: [core.reloadable], which can prevent the user from disabling an extension in the UI.

[package.authors] (default: [""])

Lists people or organizations that are considered the “authors” of the package. An optional email address may be included within angled brackets at the end of each author.

[package.repository] (default: "")

URL of the extension source repository, used for UI.

[package.keywords] (default: [""])

Array of strings that describe this extension. This can be helpful when searching for it in an Extension registry.

[package.changelog] (default: "")

Location of a CHANGELOG.MD file in the target (final) folder of the Extension, and relative to it’s root. The UI will load and show it. We can also insert the content of that file inline instead of specifying a filepath. It is important to keep the changelog updated when new versions of an extension are released and published.

For more info on writing changelog refer to Keep a Changelog

[package.readme] (default: "")

Location of README file in the target (final) folder of an extension, relative to the root. UI will load and show it. We can also insert the content of that file inline instead of specifying a filepath

[package.preview_image] (default: "")

Location of a preview image in the target (final) folder of extension, relative to the root. The preview image is shown in the “Overview” of Extensions window. A screenshot of your extension might make a good preview image.

[package.icon] (default: "")

Location of icon in the target (final) folder of extension, relative to the root. Icon is shown in Extensions window. It is recommended that it be 256x256 pixels in size

[package.target]
This section is used to describe the target platform this extension runs on - this is fairly arbitrary, but can include:
  • Operating system

  • CPU architecture

  • Python version

  • Build configuration

The Extension system will filter out extensions that doesn’t match the current environment/platform. This is particularly important for extensions published and downloaded from a remote Extension registry.

Normally you don’t need to fill this section in manually. When extension being published it will be automatically filled in with defaults, more in Publishing Extensions. But it can be overriden by setting:

package.target.kit (default: ["*"] – Kit version (without metadata), e.g. "101.0", "102.3".

package.target.config (default: ["*"] – Build config, e.g. "debug", "release".

package.target.platform (default: ["*"] – Build platform, e.g. "windows-x86_64", "linux-aarch64".

package.target.python (default: ["*"] – Python version, e.g. "cp36" (cpython 3.6). Refer to PEP 0425.

Wildcards can be used. A full example:

[package.target]
config = ["debug"]
platform = ["linux-*", "windows"]
python = ["*"]
[package.writeTarget]

This section can be used to explicitly control if [package.target] should be written. By default it is written based on rules described in Publishing Extensions. But if for some [target] a field set: package.writeTarget.[target] = true/false that tells explicitly if it should be automatically filled in.

For example if you want to write kit version as a target to make extension work only on that version, set:

[package]
writeTarget.kit = true

Or if you want your extension to work for all python versions, write:

[package]
writeTarget.python = false

List of known targets is the same as in [package.target] section: kit, config, platform, python.

[dependencies] section

This section is used to describe which extensions this extension depends on. The extension system will guarantee to enable them before it loads your extension (assuming that it doesn’t fail and not enable your extension at all). One can optionally specify version and tag per dependency and make a dependency optional.

Each entry is a name of other extension. It may or may not additional specifications such as tag, version, optional, exact:

"omni.physx" = { version="1.0", "tag"="gpu" }
"omni.foo" = { version="3.2" }
"omni.cool" = { optional = true }

Note that it is highly recommended to use versions. as it will keep extensions (and the whole application) more stable - i.e if breaking change happens in a dependency and dependents have not yet been updated, an older version can still be used. (only the major and minor parts are needed for thisaccording to semver).

optional (default: false) – will mean that if the extension system can’t resolve this dependency the extension will still be enabled. So it is expected that the extension can handle the absence of this dependency. Optional dependencies are not enabled unless they are the a non-optional dependency of some other extension which is enabled or if they are enabled explicitly (using API, settings, CLI etc.).

exact (default: false) – exact version match of extension will be used. This flag is experimental and may change.

order (default: None) – override core.order parameter of extension it depends on. Only applied if set.

[python] section

If an extension contains python modules or scripts this is where to specify them

[[python.module]]

Specifies python module(s) that are part of this extension. Multiples can be specified, notice the [[]] syntax. When an extension is enabled, modules are imported in order. Here we specify 2 python modules to be imported (import omni.hello and import omni.calculator).

Example:

[[python.module]]
name = "omni.hello"

[[python.module]]
name = "omni.calculator"
path = "."
public = true

name (required) – python module name. Think of it as what will be imported by other extensions that depend on you:

import omni.calculator

public (default: true) – If public, a module will be available to be imported by other extensions (extension folder is added to sys.path). Non-public modules have limited support and their use is not recommended.

path (default: ".") – Path to the root folder where the python module is located. If relative it is relative to extension root. Think of it as what gets added to sys.path. By default the extension root folder is added if any [[python.module]] directive is specified.

[[python.scriptFolder]]

Script folders can be added to IAppScripting, and they will be searched for when a script file path is specified to executed (with –exec or via API).

Example:

[[python.scriptFolder]]
path = "scripts"

path (required) – Path to the script folder to be added. If the path is relative it is relative to the extension root.

[native] section

Used to specify Carbonite plugins to be loaded

[[native.plugin]]

When an Extension is enabled, the Extension system will search for Carbonite plugins using path pattern and load all of them. It will also try to acquire the omni::ext::IExt interface if any of the plugins implements it. That provides an optional entry point in C++ code where your extension can be loaded.

When an extension is disabled it releases any acquired interfaces which may lead to plugins being unloaded.

Example:

[[native.plugin]]
path = "bin/${platform}/${config}/*.plugin"
recursive = false

path (required) – Path to search for Carbonite plugins, may contain wildcards and tokens (see Tokens).

recursive (default: false) – Search recursively in folders.

[[native.library]] section

Used to specify shared libraries to load when an Extension is enabled.

When an Extension is enabled the Extension system will search for native shared libraries using path and load them. This mechanism is useful to “preload” libraries needed later, avoid OS specific calls in your code, and the use of PATH/LD_LIBRARY_PATH etc to locate and load DSOs/DLLs. With this approach we just load the libraries needed directly.

When an extension is disabled it tries to unload those shared libraries.

Example:

[[native.library]]
path = "bin/${platform}/${config}/foo.dll"

path (required) – Path to search for shared libraries, may tokens (see Tokens).

[settings] section

Everything under this section is applied to root of the global Carbonite settings (carb.settings.plugin). In case of conflict, the original setting is kept.

It is good practice to namespace your settings with your extension name and put them all under exts root key, e.g.:

[settings]
exts."omni.kit.renderer.core".compatibilityMode = true

Note

Quotes are used here to distinguish between . of a toml file and . in the name of extension.

An mportant detail is that settings are applied in reverse order of extension startup (before any extensions start) and they don’t override each other. That means that a parent extension can specify settings for child extensions to use.

[[env]] section

This section is used to specify one or more environment variables to set when an extension is enabled. Just like settings, env vars are applied in reverse order of startup. They don’t by default override if already set, but override behaviour allows parent extensions to override env vars of extensions they depend on.

Example:

[[env]]
name = "HELLO"
value = "123"
isPath = false
append = false
override = false
platform = "windows-x86_64"

name (required) – Environment variable name.

value (required) – Environment variable value to set.

isPath (default: false) – Treat value as path. If relative it is relative to the extension root folder. Tokens can also be used as within any path.

append (default: false) – Append value to already set env var if any. Platform specific separator will be used.

override (default: false) – Override value of already set env var if any.

platform (default: "") – Set only if platform matches pattern. Wildcard can be used.

[fswatcher] section

Used to specify file system watcher used by the Extension system to monitor for changes in extensions and auto reload.

[fswatcher.patterns]

Specify files that are monitored.

include (default: ["*.toml", "*.py"]) – File patterns to include.

exclude (default: []) – File patterns to exclude.

Example:

[fswatcher.patterns]
include = ["*.toml", "*.py", "*.txt"]
exclude = ["*.cache"]
[fswatcher.paths]

Specify folders that are monitored. FS watcher will use OS specific API to listen for changes on those folders. You can use that setting that limit amount of subscriptions if your extensions has too many folders inside. Simple path as string pattern matching is used, where ‘*’ means any amount of symbols. ‘/’ path separator is used on all platforms. Pattern like */123* will match abc/123/def, abc/123, abc/1234.

include (default: ["*"]) – Folder path patterns to include.

exclude (default: ["*/__pycache__*", "*/.git*"]) – Folder path patterns to exclude.

Example:

[fswatcher.paths]
include = ["*/config"]
exclude = ["*/data*"]

[[test]] section

This section is read only by testing system (omni.kit.test extension) when running per extension tests. Extension test is a separate process which only enables tested extension and runs all the tests it has. Usually this section can be left empty, but extension can specify to add additional extension (which is not a part of regular dependencies, or dependency is optional), additional cmd arguments, filter out (or in) any additional stdout messages.

Extension tests run in the context of an app. An app can be empty, that makes extension test isolated and only its dependencies are enabled. Testing in an empty app is minimal recommended test coverage. Extension developer can then opt-in to be tested in other apps and fine-tune test settings per app.

Example:

[[test]]
apps = [""]
args = ["-v"]
dependencies = ["omni.kit.capture"]
pythonTests.include = ["omni.foo.*"]
pythonTests.exclude = []
timeout = 180
parallelizable = true
stdoutFailPatterns.include = ["*[error]*", "*[fatal]*"]
stdoutFailPatterns.exclude = [
    "*Leaking graphics objects*",  # Exclude grahics leaks until fixed
]

apps – List of apps to use that configuration for. Used in case there are multiple [[test]] entries. Wildcards are supported. Defaults to [""] which is an empty app.

args – Additional cmd arguments to pass into extension test process.

dependencies – List of additional extensions to enable when running extension test.

pythonTests.include – List of tests to run. If empty python modules used instead ([[python.module]]). Since all tests names start with module they defined in. Can contain wildcard symbol.

pythonTests.exclude – List of tests to exclude from running. Can contain wildcard symbol.

timeout (default: 180) – Test process timeout (in seconds).

parallelizable (default: true) – Test process can run in parallell relative to other test processes.

stdoutFailPatterns.include – List of additional patterns to search in stdout and treat as a failure. Can contain wildcard symbol.

stdoutFailPatterns.exclude – List of additional patterns to exclude from being a test failure. Can contain wildcard symbol.

Config Filters

Any part of config can be filtered based on current platform or build configuration. Use "filter:platform"."[platform_name]" or "filter:config"."[build_config]" pair of keys. Anything under those keys will be merged on top of tree they are located in (or filtered out if doesn’t apply).

filter

values

platform

windows-x86_64, linux-x86_64

config

debug, release

To understand look at example:

[dependencies]
"omni.foo" = {}
"filter:platform"."windows-x86_64"."omni.fox" = {}
"filter:platform"."linux-x86_64"."omni.owl" = {}
"filter:config"."debug"."omni.cat" = {}

After loading that extension on windows in debug build it will resolve to:

[dependencies]
"omni.foo" = {}
"omni.fox" = {}
"omni.cat" = {}

Note

You can debug it by running in debug mode, with --/app/extensions/debugMode=1 setting and looking into log file.

Example

Here is a full example of an extension.toml file:

[core]
reloadable = true
order = 0

[package]
version = "0.1.0"
category = "Example"
feature = false
app = false
title = "The Best Package"
description = "long and boring text.."
authors = ["John Smith <jsmith@email.com>"]
repository = "https://gitlab-master.nvidia.com/omniverse/kit"
keywords = ["banana", "apple"]
changelog = "docs/CHANGELOG.md"
readme = "docs/README.md"
preview_image = "data/preview.png"
icon = "data/icon.png"

# writeTarget.kit = true
# writeTarget.platform = true
# writeTarget.config = true
# writeTarget.python = true

[dependencies]
"omni.physx" = { version="1.0", "tag"="gpu" }
"omni.foo" = {}

# Modules are loaded in order. Here we specify 2 python modules to be imported (``import hello`` and ``import omni.physx``).
[[python.module]]
name = "hello"
path = "."
public = false

[[python.module]]
name = "omni.physx"

[[python.scriptFolder]]
path = "scripts"

# Native section, used if extension contains any Carbonite plugins to be loaded
[[native.plugin]]
path = "bin/${platform}/${config}/*.plugin"
recursive = false  # false is default, hence it is optional

# Library section. Shared libraries will be loaded when the extension is enabled, note [[]] toml syntax for array of objects.
[[native.library]]
path = "bin/${platform}/${config}/foo.dll"

# Settings. They are applied on the root of global settings. In case of conflict original settings are kept.
[settings]
exts."omni.kit.renderer.core".compatibilityMode = true

# Environment variables. Example of adding "data" folder in extension root to PATH on windows:
[[env]]
name = "PATH"
value = "data"
isPath = true
append = true
platform = "windows-x86_64"

# Fs Watcher patterns and folders. Specify which files are monitored for changes to reload an extension. Use wildcard for string matching.
[fswatcher]
patterns.include = ["*.toml", "*.py"]
patterns.exclude = []
paths.include = ["*"]
paths.exclude = ["*/__pycache__*", "*/.git*"]

Extension Enabling/Disabling

Extensions can be enabled and disabled at runtime using the provided API. Default Create comes with an Extension Manager UI which shows all available extensions and allow user to toggle them. An App configuration file can also used to control which extensions are to be enabled.

You may also use command line arguments to the Kit executable (or any Omniverse App based on Kit) to enable specific extensions:

Example: > kit.exe --empty --enable omni.kit.window.console --enable omni.kit.window.extensions

--empty tells Kit not to use default app configuration. --enable adds extension to “enabled list”. Command above will start only extensions needed to show those 2 windows.

Python Modules

Enabling an extension loads the python modules specified and searches for children of omni.ext.IExt class. They are instantiated and the on_startup method is called, e.g.:

hello.py

import omni.ext

class MyExt(omni.ext.IExt):
    def on_startup(self, ext_id):
        pass

    def on_shutdown(self):
        pass

When an extension is disabled on_shutdown is called and all references to the extension object are released.

Native Plugins

Enabling an extension loads all Carbonite plugin specified by search masks in the native.plugin section. If one or more plugins implement omni::ext::IExt interface - is is acquired and the onStartup method is called. When an extension is disabled onShutdown is called and the interface is released.

Settings

Settings to be applied when an extension is enabled can be specified in the settings section. They are applied on the root of global settings. In case of conflict the original settings are kept. It is recommended to use path exts/[extension_name] for extension settings, but in general any path can be used.

It is also good practice to document settings in the extension.toml file, which makes it a great place to discover which settings a particular extension supports.

Tokens

When extension is enabled it sets tokens into Carbonite ITokens interface with a path to extension root folder. E.g. for extension omni.foo-bar tokens ${omni.foo} and ${omni.foo-bar} are set.

Extensions Manager

Reloading

Extensions can be hot reloaded. The Extension system monitors the file system for changes to enabled extensions. If it finds any, the extensions are disabled and enabled again (which can involve reloading large parts of the dependency tree). That allows live editing of python code and recompilation of C++ plugins.

Use fswatcher.patterns and fswatcher.paths config settings (see above) to control which files change triggers reloading.

Use reloadable config setting to disable reloading. That will also block the reloading of all extensions this extension depends on. The extension can still be unloaded using API.

New extensions can also be added and removed at runtime.

Extension interfaces

The Extension manager is implemented in omni.ext.plugin, with an interface: omni::ext::IExtensions (C++) omni.ext module (python)

It is loaded by omni.kit.app and you can get an extension manager instance using it’s interface: omni::kit::IApp (C++) omni.kit.app module (python)

Runtime Information

At runtime a user can query various pieces of information about each extension. Use omni::ext::IExtensions::getExtensionDict() to get a dictionary for each extension with all the relevant information. For python use omni.ext.ExtensionManager.get_extension_dict().

This dictionary contains:

  1. Everything the extension.toml contains under the same path

  2. It has an additional state section which contains:

    1. state/enabled (bool): Indicates if the extension is currently enabled.

    2. state/reloadable (bool): Indicates if the extension can be reloaded (used in the UI to disable extension unloading/reloading)

Hooks

Both the C++ and python APIs for the Extension system provide a way to hook into certain actions/phases of the Extension System to enable extending it. If you register a hook like this:

def on_before_ext_enabled(self, ext_id: str, *_):
    pass

manager = omni.kit.app.get_app_interface().get_extension_manager()
self._hook = self._manager.get_hooks().create_extension_state_change_hook(
    self.on_before_ext_enabled,
    omni.ext.ExtensionStateChangeType.BEFORE_EXTENSION_ENABLE,
    ext_dict_path="python/pipapi",
    hook_name="python.pipapi",
)

..your callback will be called before each extension that contain the “python/pipapi” key in a config file is enabled. This allows us to write extension that extend the Extension system. They can define their own configuration settings and react to them when extensions which contain those settings get loaded.

Other Settings

/app/extensions/disableStartup (default: false)

Special mode when extensions doesn’t start (python and C++ startup functions are not called). Everything else works as usual. One usage might be to warm up everything and get extensions downloaded. Another use case is getting python environment setup without starting anything.

/app/extensions/precacheMode (default: false)

Special mode when all dependencies are solved and extensions downloaded, then app exits. It is useful for precaching all extensions before running an app to get everything downloaded and check that all dependencies are correct.

/app/extensions/debugMode (default: false)

Output more debug information into info logging channel.

/app/extensions/registryEnabled (default: true)

Disable falling back to extension registry when couldn’t resolve all dependencies and fail immediately.

/app/extensions/skipPublishVerification (default: false)

Skip extension verification before publishing. Use wisely.

/app/extensions/excluded (default: [])

List of extensions to exclude from startup. Can be with or without version. Before solving startup order all those extensions are removed from all dependencies.

/app/extensions/preferLocalVersions (default: true)

If true prefer local extension versions over remote ones during dependency solving. Otherwise all treated equally and it is likely to get newer versions selected and downloaded.

/app/extensions/syncRegistryOnStartup (default: false)

Force sync with registry on startup. Otherwise registry is only enabled if dependency solving fails (something missing). Also –update-exts` command line switch enables that behavior.

Extension Registries

The Extension system supports adding external registry providers for publishing extensions to, and pulling extensions from. By default Kit comes with omni.kit.registry.nucleus extension (omni.kit.registry.nucleus) which adds support for Nucleus as an extension registry.

When an extension is enabled the dependency solver resolves all dependencies. If a dependency is missing in the local cache it will ask the registry for a particular extension and it will be downloaded and installed at runtime. Installation is just unpacking of a zip archive into cache folder (app/extensions/registryCache setting).

The Extension system will only enable the Extension registry when it can’t find all extensions locally. At that moment it tries to enable an extension specified in setting: app/extensions/registryExtension, which by default is omni.kit.registry.nucleus.

The Registry system can be completely disabled with app/extensions/registryEnabled setting.

The Extension manager provides an API to add other extension registries and query any existing ones ( omni::ext::IExtensions::addRegistryProvider, omni::ext::IExtensions::getRegistryProviderCount etc).

Multiple registries can be configured to be used ath the same time. They are uniquely identified with a name. Setting app/extensions/registryPublishDefault sets which one to use by default when publishing and unpublishing extensions. The API provides a way to explicitly pass the registry to use.

Publishing Extensions

To publish your extension to the registry use kit.exe --publish CLI command. E.g.

Example: > kit.exe --publish omni.my.ext-tag

If there is more than one version of this extension available it will produce an error saying you need to specify which one to publish.

Example: > kit.exe --publish omni.my.ext-tag-1.2.0

To specify in which registry to publish, override default registry name:

Example: > kit.exe --publish omni.my.ext-tag-1.2.0 --/app/extensions/registryPublishDefault="kit/mr"

If the extension already exist in the registry it will fail. To force overwrite use additional --publish-overwrite argument:

Example: > kit.exe --publish omni.my.ext --publish-overwrite

Version must be specified in a config file for publishing to succeed.

All [package.target] config subfields if not specified are filled in automatically:

  • If the extension contains any [python] fields [package.target.python] is filled with current python version.

  • If the extension contains any [native] or [native.library] fields [package.target.platform] and [package.target.config] is filled with current platform information.

An Extension package name will look like this: [ext_name]-[ext_tag]-[major].[minor].[patch]-[prerelase]+[build]. Where

  • [ext_name]-[ext_tag] is the extension name (initially comes from extension folder).

  • [ext_tag]-[major].[minor].[patch]-[prerelase] if [package.version] field of a config.

  • [build] is composed from [package.target]].

Pulling Extensions

There are multiple ways to get Extensions (both new Extensions and updated versions of existing Extensions) from a registry:

  1. Use the UI provided by extension: omni.kit.window.extensions

  2. If extensions specified in the app config file or required through dependencies are missing from local cache the system will attempt to sync with the registry and pull them. That means e.g if you have version “1.2.0” of an Extension locally it won’t be updated to “1.2.1” automatically, because “1.2.0” satisfies the dependencies. To force an update run Kit with --update-exts flag:

Example: > kit.exe --update-exts

Pre-downloading Extensions

You can also run Kit without starting any extensions. The benefit of this is that they will be downloaded and cached for the next run. To do that run Kit with --disable-ext-startup flag:

Example: > kit.exe --disable-ext-startup

Authentication and Users

Omniverse Client library is used to all the operations with nucleus registry. Both syncing, downloading and publishing extensions requires signing in. To automate it separate 2 accounts can be explicitly provided to be used for read and write operations.

Read and write user accounts can be set for omni.kit.registry.nucleus extension. Read user account is used for syncing with registry and downloading extensions. Write user account is used for publishing or unpublishing extensions. If no user is set it defaults to regular sign in using browser.

By default kit comes with default read account set, to eliminate need for users to sign in to just pull extensions.

Read user account setting: /exts/omni.kit.registry.nucleus/omniReadAuth Write user account setting: /exts/omni.kit.registry.nucleus/omniWriteAuth Format is: “user:pass”, e.g. --/exts/omni.kit.registry.nucleus/omniReadAuth="bob:12345".

Building Extensions

Extensions are a runtime concept. This guide doesn’t describe how to build them and how to build other extensions which may depend on a specific extension at build-time. One can use different tools and setups for that, we do however have some best practices. The Best source of information on that currently are:

A good place to start might be the omni.example.hello extension (and other extensions). Copy and rename it to create a new extension.

We also strive to use folder linking as much as possible. Meaning we don’t copy python files and configs from the source folder to the target (build) folder, but link them. That allows changes to the files under version control to be immediately reflected even at runtime. Unfortunately we can’t link files, because of Windows limitations, so folder linking is used. That adds some verbosity to the way the folder structure is organized.

For example for simple python only extension, we link the whole folder:

source/extensions/omni.example.hello/target – [linked to] –> _build/windows-x86_64/debug/exts/omni.example.hello

Folder named “target” gets linked automatically by project_ext() lua function.

For an extension with binary components we link the config folder:

source/extensions/omni.example.hello/config – [linked to] –> _build/windows-x86_64/debug/exts/omni.example.hello/config

And we can manually specify other parts to link in the premake file: repo_build.prebuild_link { "folder", ext.target_dir.."/folder" }

When working with the build system it is always a good idea to look at what the final _build/windows-x86_64/debug/exts folder looks like, which folder links are there, where they point to, which files were copied there etc. Remember that the goal is to produce one extension folder which will then potentially will be zipped and published. Folder links are just zipped as is, as if they were actual folders.