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.

Specify keyword: package.keyword = ["app"] in .kit file to mark your extension as an app, one users can launch from. It also adds a “launch” button to that extension in the UI of the extension browser.

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 Folders

Extensions are automatically searched for in specified folders. A few folders are automatically added by the app configuration. 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 (there are other setting paths still in use which do the same, but outdated like: /app/extensions/folder2).

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

You may also specify a direct path to a specific extension, with the /app/exts/paths setting or the --ext-path [PATH] CLI parameter

Extension Discovery

The Extension system constantly monitor folders for changes and automatically syncs all extensions found in the specified folders. Any subfolder which contains an extension.toml file 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.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.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.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.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 = ["*"]

[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"]

Example

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

[core]
reloadable = true
order = 0

[package]
version = "0.1.0"
category = "Example"
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"

[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. Specify which files are monitored for changes to reload an extension.
[fswatcher.patterns]
include = ["*.toml", "*.py"]
exclude = []

Enable/Disable

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.

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 config setting (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.

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 (../../source/extensions/omni.kit.registry.nucleus/docs/index) 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

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.