Skip to main content

CEP 11 - Define the menuinst standard

Title Define the menuinst standard
Status Accepted
Author(s) Jaime Rodríguez-Guerra <jaime.rogue@gmail.com>
Created Oct 14, 2021
Updated Jul 28, 2023
Discussion conda-incubator/ceps#8
Implementation conda/menuinst@cep-devel

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119 when, and only when, they appear in all capitals, as shown here.

Abstract

menuinst is a library used by conda to install menu items that point to commands provided by conda packages. It operates by discovering certain JSON files located in $PREFIX/Menu after linking the package to the environment.

This library has primarily targeted Windows. The original project supported Linux and MacOS, but menuinst was never used in practice on those platforms. As a result, the required JSON metadata diverged significantly in each platform, and the implementations were not kept up to date.

This CEP will attempt to standardize the menuinst interface by:

  1. Providing a unified metadata schema for all platforms so a single document contains all the metadata required to create shortcuts in all platforms.
  2. Enumerating the expected behavior for different configurations.
  3. Defining a programmatic interface for implementers (CLI / API).

The full JSON schema is defined in this document, but here you can see a simplified overview of all possible keys and their default values:

{
"$id": "https://schemas.conda.io/menuinst-1.schema.json",
"$schema": "https://json-schema.org/draft-07/schema",
"menu_name": "REQUIRED",
"menu_items": [
{
"name": "REQUIRED",
"description": "REQUIRED",
"command": [
"REQUIRED",
],
"icon": None, # path to ico / png / icns file
"precreate": None, # command to run before the shortcut is created
"precommand": None, # command to run before activation and 'command'
"working_dir": None, # starting working location for the process
"activate": true, # activate conda environment before running 'command'
"terminal": false, # open in terminal and leave it open
"platforms": {
# To create the menu item for a fiven platform, the key must be present in this
# dictionary. Presence is enough; the value can just be the empty dictionary: {}.
"linux": {
# See XDG Desktop standard for details
# https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys
"Categories": None,
"DBusActivatable": None,
"GenericName": None,
"Hidden": None,
"Implements": None,
"Keywords": None,
"MimeType": None,
"NoDisplay": None,
"NotShowIn": None,
"OnlyShowIn": None,
"PrefersNonDefaultGPU": None,
"StartupNotify": None,
"StartupWMClass": None,
"TryExec": None,
#: Map of custom MIME types to their corresponding glob patterns (e.g. ``*.txt``).
"glob_patterns": None
},
"osx": {
# See Apple docs for CF* and LS* variables
# CF*: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# LS*: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html
"CFBundleDisplayName": None,
"CFBundleIdentifier": None,
"CFBundleName": None,
"CFBundleSpokenName": None,
"CFBundleVersion": None,
"CFBundleURLTypes": None,
"CFBundleDocumentTypes": None,
"LSApplicationCategoryType": None,
"LSBackgroundOnly": None,
"LSEnvironment": None,
"LSMinimumSystemVersion": None,
"LSMultipleInstancesProhibited": None,
"LSRequiresNativeExecution": None,
"UTExportedTypeDeclarations": None,
"UTImportedTypeDeclarations": None,
#: list of permissions to request for the app
#: see https://developer.apple.com/documentation/bundleresources/entitlements
"entitlements": None,
#: symlink a file (usually the executable in 'command') into the .app directory
"link_in_bundle": None,
#: shell logic that will run when an Apple event is received
"event_handler": None,
},
"win": {
"desktop": true, # create desktop location
"quicklaunch": true, # create quick launch shortcut too
"file_extensions": None, # file extensions to associate with shortcut in registry
"url_protocols": None, # URI protocols to associate with shortcut in registry
"app_user_model_id": None, # Identifier used to associate processes with a taskbar icon
}
}
}
]
}

Note that each platforms sub-dictionary (linux, macos, win) can override the global values of their menu_items[*] entry (e.g. redefining command to adjust the shell syntax).

Each JSON file MUST be validated against its $id schema at build time; e.g in conda-build.

Placeholders

Each platform MUST provide these placeholders, to be used in the value of any str-accepting key. To be replaced, they MUST be wrapped in double curly braces: {{ NAME }}.

PlaceholderValue
BASE_PREFIXPath to the base Python location. In conda terms, this is the base environment
DISTRIBUTION_NAMEName of the base prefix directory; e.g. if BASE_PREFIX is /opt/my-project, this is my-project.
PREFIXPath to the target Python location. In conda terms, this is the path to the environment that contains the JSON file for this menu item. In some cases, it might be the same as BASE_PREFIX.
ENV_NAMESame as DISTRIBUTION_NAME, but for PREFIX.
PYTHONPath to the python executable in PREFIX.
BASE_PYTHONPath to the python executable in BASE_PREFIX.
MENU_DIRPath to the Menu directory in PREFIX.
MENU_ITEM_LOCATIONPath to the main menu item artifact once installed. On Linux, this is the path to the .desktop file, on macOS it is the path to the .app directory, and on Windows it is the path to the Start Menu .lnk file.
BIN_DIRPath to the bin (Unix) or Library/bin (Windows) directories in PREFIX.
PY_VERPython major.minor version in PREFIX.
SP_DIRPath to Python's site-packages directory in PREFIX.
HOMEPath to the user directory (~).
ICON_EXTExtension of the icon file expected by each platform. png in Linux, icns in macOS, and ico in Windows. Note the dot is not included.
macOS-only
PYTHONAPPPath to the python executable installed in PREFIX, assuming the python.app conda package is installed. Equivalent to {{ PREFIX }}/python.app/Contents/MacOS/python.
Windows-only
SCRIPTS_DIRPath to the Scripts directory in PREFIX.
BASE_PYTHONWPath to the pythonw.exe executable in BASE_PREFIX.
PYTHONWPath to the pythonw.exe executable in PREFIX.

Packaging guidelines

conda packages that wish to create a shortcut at install time MUST provide a JSON file such that:

  • The JSON contents MUST pass schema validation.
  • The JSON file MUST be placed under $PREFIX/Menu.
  • The JSON filename MUST be <package-name>.json.
  • Packaging tools (e.g. conda-build) MUST check the above conditions are met when the package is being created.

One example of a properly placed JSON file would be $PREFIX/Menu/my-package.json included in the my-package-1.2.3-h123abc.conda artifact.

Expected behavior

Each platform MUST place the menu artifacts in these target locations:

Operating systemArtifact typeUser locationSystem locationNotes
Linux.desktop file~/.local/share/applications/usr/local/share/applicationsSome other user files are modified
macOS.app directory~/Applications/Applications
Windows.lnk file{{ menu_name }} directory inside Start Menu, Desktop, and/or Quick LaunchStart MenuThese locations are customizable and configured in the Windows registry.
  • On Linux, little needs to be done because XDG delegates the responsibility to the desktop manager. The implementer only needs to create the .desktop file and adjust/add the menu XML file(s).
  • On macOS, we had to come up with some ideas. The shortcut is actually an .app directory. Implementers must follow Apple's guidelines. See Addendum B for implementation details.
  • On Windows, .lnk files are created with the Windows API. File type and URL protocol association is done in the Windows registry.

Some installations might provide two modes: "Current user only", and "All users". This option is not surfaced in the JSON metadata, but might be requested at creation time in the CLI or API. This means that implementers MUST be able to handle both user locations and system locations, as detailed above. In particular, in-process permission elevation needs to be considered.

When a package is removed, the file artifacts MUST be deleted too. If changes were done in other resources (XML files on Linux, Registry on Windows), these MUST be undone too.

CLI interface

The implementer CLI is not defined in this document. However, the integrations with constructor SHOULD be standardized if they are going to be kept in use.

The proposed CLI (inspired by what's already in use to introduce minimal changes) is:

${IMPLEMENTER} constructor --prefix ${PREFIX} [--base-prefix ${BASE_PREFIX}] [--mode user|system] [--make-menus | --rm-menus] [pkg-name ...]
  • --make-menus will create the menu items for the JSON files found in $PREFIX/Menu.
  • --rm-menus will uninstall the corresponding menu items from the system.
  • If values are passed next to these two flags, only the JSON files that match those package names will be handled. Others will be ignored.
  • --base-prefix is optional and defaults to the value passed to --prefix. It is only needed when IMPLEMENTER is running from a location other than --prefix (e.g. base vs a custom environment, or system Python and a virtual environment).
  • --mode is optional and defaults to the mark provided by the --base-prefix location. If a .nonadmin file is present there, mode=user will be assumed. Otherwise, mode=system will be assumed, with a fallback to mode=user if necessary.

Alternatively, the constructor subcommand needs for menus can be dropped if IMPLEMENTER supports new settings and/or CLI flags in the create | install commands. Namely:

  • base_prefix: override the assumed base environment location. This is nowadays available as root_prefix but overriding this with environment variables (via CONDA_ROOT_PREFIX) is buggy in conda and needs to be fixed.
  • Extend shortcuts with the ability of accepting values (true, false, or a list of strings).
    • --shortcuts would set shortcuts=True, which is the default otherwise.
    • --no-shorcuts would set shortcuts=False.
    • --shortcuts pkg1 pkg2 ... would set shortcuts=[pkg1, pkg2, ...], which would instruct IMPLEMENTER to handle menu item creation or removal for those packages only.

Backwards compatibility

Windows users do depend on the existing menuinst 1.x "schema". There are a lot of packages that use it. This (unversioned) document needs to be kept around and respected. In the absence of the $schema or $id keys, it will be assumed that the metadata is built with the legacy schema.

See Addendum A below for a best effort in documenting it.

References

All CEPs are explicitly CC0 1.0 Universal.


Addendum A

The required metadata for each platform is documented in the menuinst wiki. However, only Windows is really supported by the tool. This asymmetrical growth has allowed Windows to grow an ad-hoc specification that doesn't really translate well to other platforms.

The overall schema seems to be:

{
"menu_name": str,
"menu_items": list of dict,
}

Unfortunately, each menu item dict (let's call it MenuItem) takes a different form in each platform.

{
["system" | "script" | "pyscript" | "pywscript" | "webbrowser"]: str,
"scriptargument": str,
"scriptarguments": list of str,
"name": str,
"workdir": str,
"icon": str,
"desktop": bool,
"quicklaunch": bool,
}

Currently allowed placeholders are:

  • ${PREFIX}: Python environment prefix
  • ${ROOT_PREFIX}: Python environment prefix of root (conda or otherwise) installation
  • ${PYTHON_SCRIPTS}: Scripts folder in Python environment, ${PREFIX}/Scripts
  • ${MENU_DIR}: Folder for menu config and icon files, ${PREFIX}/Menu
  • ${PERSONALDIR}: Not sure
  • ${USERPROFILE}: User's home folder
  • ${ENV_NAME}: The environment in which this shortcut lives.
  • ${DISTRIBUTION_NAME}: The name of the folder of the root prefix, for example "Miniconda" if distribution installed at "C:\Users\Miniconda".
  • ${PY_VER}: The Python major version only. This is taken from the root prefix. Used generally for placing shortcuts under a menu of a parent installation.
  • ${PLATFORM}: one of (32-bit) or (64-bit). This is taken from the root prefix. Used generally for placing shortcuts under a menu of a parent installation.
{
"cmd": str,
"name": str,
"icns": str,
}

Currently allowed placeholders are:

  • ${BIN_DIR}: PREFIX/bin
  • ${MENU_DIR}: PREFIX/Menu
{
"cmd": list of str,
"id": str,
"name": str,
"comment": str.
"terminal": bool,
"icon": str,
},

On Linux, only cmd can take two special placeholders {{FILEBROWSER}} and {{WEBBROWSER}}, which are replaced by the default Desktop file explorer, and the default web browser, respectively.

Identified problems

The command interface

Windows has several ways to specify which command should be run with the shortcut:

  • system + scriptargument[s]: path to executable plus its arguments.
  • script + scriptargument[s]: same as above, but the executable is run in a subprocess after invoking ROOT_PYTHON cwp.py PREFIX.
  • pyscript: hardcodes script to be PREFIX/python.exe and takes the value as the first (and only) argument.
  • pywscript: same as above, but uses pythonw.exe as the launcher to, theoretically, avoid launching a console window next to your application.
  • webbrowser: alias to PREFIX/python -m webbrowser -t URL.

On Linux the command is specified with cmd, expressed as a list of strings. On MacOS, cmd is also taken, but in this case it's expected to be a raw string.

The icon key

Windows and Linux expect icon. MacOS expects icns. Each platform requires a different file format, but that can be arranged with placeholders.

Standardize the placeholders

Allowed placeholders vary vastly across platforms. A common subset must be identified and implemented. Per-platform options are allowed but only when strictly necessary.

Addendum B: Implementation details in macOS

  • Most of the macOS-specific settings map to the .app's Info.plist key-value pairs.
  • The shell script with the precommand + activate + command logic is located in <NAME>.app/Contents/MacOS/<NAME>-script.
  • A binary launcher is required for correct system integration (see reasons conda/menuinst#123). This is placed at <NAME>.app/Contents/MacOS/<NAME>. The proposed launcher simply guesses its own location to find the *-script file, which is spawned in a subprocess.
  • In some cases, if an external binary is required, it needs to be symlinked into the .app directory to ensure keyboard integrations work (see conda/menuinst#122).
  • URL protocol association requires special support in the binary launcher. Implementers can choose how to implement it. See this issue and this PR for ideas.