|
| 1 | +<table> |
| 2 | +<tr><td> Title </td><td> Define the <code>menuinst</code> standard</td> |
| 3 | +<tr><td> Status </td><td> Accepted</td></tr> |
| 4 | +< tr>< td> Author(s) </ td>< td> Jaime Rodríguez-Guerra <[email protected]></ td></ tr> |
| 5 | +<tr><td> Created </td><td> Oct 14, 2021</td></tr> |
| 6 | +<tr><td> Updated </td><td> Jul 28, 2023</td></tr> |
| 7 | +<tr><td> Discussion </td><td> <a href="https://github.com/conda-incubator/ceps/pull/8" target="_blank">conda-incubator/ceps#8</a> </td></tr> |
| 8 | +<tr><td> Implementation </td><td> <a href="https://github.com/conda/menuinst/tree/cep-devel" target="_blank"><code>conda/menuinst</code>@<code>cep-devel</code></a> </td></tr> |
| 9 | +</table> |
| 10 | + |
| 11 | +> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", |
| 12 | + "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as |
| 13 | + described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here. |
| 14 | + |
| 15 | +## Abstract |
| 16 | + |
| 17 | +`menuinst` is a library used by `conda` to install menu items that point to commands provided by |
| 18 | +`conda` packages. It operates by discovering certain JSON files located in `$PREFIX/Menu` after |
| 19 | +linking the package to the environment. |
| 20 | + |
| 21 | +This library has primarily targetted Windows. The original project supported Linux and MacOS, but |
| 22 | +`menuinst` was never used in practice on those platforms. As a result, the required JSON metadata |
| 23 | +diverged significantly in each platform, and the implementations were not kept up to date. |
| 24 | + |
| 25 | +This CEP will attempt to standardize the `menuinst` interface by: |
| 26 | + |
| 27 | +1. Providing a unified metadata schema for all platforms so a single document contains all the |
| 28 | + metadata required to create shorcuts in all platforms. |
| 29 | +2. Enumerating the expected behavior for different configurations. |
| 30 | +3. Defining a programmatic interface for implementors (CLI / API). |
| 31 | + |
| 32 | +## Menu metadata schema |
| 33 | + |
| 34 | +The full JSON schema is defined in [this document][menuinst-json-schema], but here you can see a |
| 35 | +simplified overview of all possible keys and their default values: |
| 36 | + |
| 37 | +```python |
| 38 | +{ |
| 39 | + "$id": "https://schemas.conda.io/menuinst-1.schema.json", |
| 40 | + "$schema": "https://json-schema.org/draft-07/schema", |
| 41 | + "menu_name": "REQUIRED", |
| 42 | + "menu_items": [ |
| 43 | + { |
| 44 | + "name": "REQUIRED", |
| 45 | + "description": "REQUIRED", |
| 46 | + "command": [ |
| 47 | + "REQUIRED", |
| 48 | + ], |
| 49 | + "icon": None, # path to ico / png / icns file |
| 50 | + "precreate": None, # command to run before the shortcut is created |
| 51 | + "precommand": None, # command to run before activation and 'command' |
| 52 | + "working_dir": None, # starting working location for the process |
| 53 | + "activate": true, # activate conda environment before running 'command' |
| 54 | + "terminal": false, # open in terminal and leave it open |
| 55 | + "platforms": { |
| 56 | + # To create the menu item for a fiven platform, the key must be present in this |
| 57 | + # dictionary. Presence is enough; the value can just be the empty dictionary: {}. |
| 58 | + "linux": { |
| 59 | + # See XDG Desktop standard for details |
| 60 | + # https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys |
| 61 | + "Categories": None, |
| 62 | + "DBusActivatable": None, |
| 63 | + "GenericName": None, |
| 64 | + "Hidden": None, |
| 65 | + "Implements": None, |
| 66 | + "Keywords": None, |
| 67 | + "MimeType": None, |
| 68 | + "NoDisplay": None, |
| 69 | + "NotShowIn": None, |
| 70 | + "OnlyShowIn": None, |
| 71 | + "PrefersNonDefaultGPU": None, |
| 72 | + "StartupNotify": None, |
| 73 | + "StartupWMClass": None, |
| 74 | + "TryExec": None, |
| 75 | + #: Map of custom MIME types to their corresponding glob patterns (e.g. ``*.txt``). |
| 76 | + "glob_patterns": None |
| 77 | + }, |
| 78 | + "osx": { |
| 79 | + # See Apple docs for CF* and LS* variables |
| 80 | + # CF*: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html |
| 81 | + # LS*: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html |
| 82 | + "CFBundleDisplayName": None, |
| 83 | + "CFBundleIdentifier": None, |
| 84 | + "CFBundleName": None, |
| 85 | + "CFBundleSpokenName": None, |
| 86 | + "CFBundleVersion": None, |
| 87 | + "CFBundleURLTypes": None, |
| 88 | + "CFBundleDocumentTypes": None, |
| 89 | + "LSApplicationCategoryType": None, |
| 90 | + "LSBackgroundOnly": None, |
| 91 | + "LSEnvironment": None, |
| 92 | + "LSMinimumSystemVersion": None, |
| 93 | + "LSMultipleInstancesProhibited": None, |
| 94 | + "LSRequiresNativeExecution": None, |
| 95 | + "UTExportedTypeDeclarations": None, |
| 96 | + "UTImportedTypeDeclarations": None, |
| 97 | + #: list of permissions to request for the app |
| 98 | + #: see https://developer.apple.com/documentation/bundleresources/entitlements |
| 99 | + "entitlements": None, |
| 100 | + #: symlink a file (usually the executable in 'command') into the .app directory |
| 101 | + "link_in_bundle": None, |
| 102 | + #: shell logic that will run when an Apple event is received |
| 103 | + "event_handler": None, |
| 104 | + }, |
| 105 | + "win": { |
| 106 | + "desktop": true, # create desktop location |
| 107 | + "quicklaunch": true, # create quick launch shortcut too |
| 108 | + "file_extensions": None, # file extensions to associate with shortcut in registry |
| 109 | + "url_protocols": None, # URI protocols to associate with shorcut in registry |
| 110 | + "app_user_model_id": None, # Identifier used to associate processes with a taskbar icon |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + ] |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +Note that each `platforms` sub-dictionary (`linux`, `macos`, `win`) can override the global values |
| 119 | +of their `menu_items[*]` entry (e.g. redefining `command` to adjust the shell syntax). |
| 120 | + |
| 121 | +Each JSON file MUST be validated against its `$id` schema at build time; e.g in `conda-build`. |
| 122 | + |
| 123 | +## Placeholders |
| 124 | + |
| 125 | +Each platform MUST provide these placeholders, to be used in the value of any `str`-accepting key. |
| 126 | +To be replaced, they MUST be wrapped in double curly braces: `{{ NAME }}`. |
| 127 | + |
| 128 | +Placeholder | Value |
| 129 | +------------|------- |
| 130 | +`BASE_PREFIX` | Path to the base Python location. In `conda` terms, this is the `base` environment |
| 131 | +`DISTRIBUTION_NAME` | Name of the base prefix directory; e.g. if `BASE_PREFIX` is `/opt/my-project`, this is `my-project`. |
| 132 | +`PREFIX` | Path 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`. |
| 133 | +`ENV_NAME` | Same as `DISTRIBUTION_NAME`, but for `PREFIX`. |
| 134 | +`PYTHON` | Path to the `python` executable in `PREFIX`. |
| 135 | +`BASE_PYTHON` | Path to the `python` executable in `BASE_PREFIX`. |
| 136 | +`MENU_DIR` | Path to the `Menu` directory in `PREFIX`. |
| 137 | +`MENU_ITEM_LOCATION` | Path 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. |
| 138 | +`BIN_DIR` | Path to the `bin` (Unix) or `Library/bin` (Windows) directories in `PREFIX`. |
| 139 | +`PY_VER` | Python `major.minor` version in `PREFIX`. |
| 140 | +`SP_DIR` | Path to Python's `site-packages` directory in `PREFIX`. |
| 141 | +`HOME` | Path to the user directory (`~`). |
| 142 | +`ICON_EXT` | Extension of the icon file expected by each platform. `png` in Linux, `icns` in macOS, and `ico` in Windows. Note the dot is _not_ included. |
| 143 | +**macOS-only** | |
| 144 | +`PYTHONAPP` | Path to the `python` executable installed in `PREFIX`, assuming the `python.app` conda package is installed. Equivalent to `{{ PREFIX }}/python.app/Contents/MacOS/python`. |
| 145 | +**Windows-only** | |
| 146 | +`SCRIPTS_DIR` | Path to the `Scripts` directory in `PREFIX`. |
| 147 | +`BASE_PYTHONW` | Path to the `pythonw.exe` executable in `BASE_PREFIX`. |
| 148 | +`PYTHONW` | Path to the `pythonw.exe` executable in `PREFIX`. |
| 149 | + |
| 150 | +## Packaging guidelines |
| 151 | + |
| 152 | +`conda` packages that wish to create a shortcut at install time MUST provide a JSON file such that: |
| 153 | + |
| 154 | +- The JSON contents MUST pass schema validation. |
| 155 | +- The JSON file MUST be placed under `$PREFIX/Menu`. |
| 156 | +- The JSON filename MUST be `<package-name>.json`. |
| 157 | +- Packaging tools (e.g. `conda-build`) MUST check the above conditions are met when the package is |
| 158 | + being created. |
| 159 | + |
| 160 | +> One example of a properly placed JSON file would be `$PREFIX/Menu/my-package.json` included |
| 161 | +> in the `my-package-1.2.3-h123abc.conda` artifact. |
| 162 | +
|
| 163 | +## Expected behavior |
| 164 | + |
| 165 | +Each platform MUST place the menu artifacts in these target locations: |
| 166 | + |
| 167 | +Operating system | Artifact type | User location | System location | Notes |
| 168 | +-----------------|---------------|---------------|-----------------|------- |
| 169 | +Linux | `.desktop` file | `~/.local/share/applications` | `/usr/local/share/applications` | Some other user files are modified |
| 170 | +macOS | `.app` directory | `~/Applications` | `/Applications` | |
| 171 | +Windows | `.lnk` file | `{{ menu_name }}` directory inside Start Menu, Desktop, and/or Quick Launch | Start Menu | These locations are customizable and configured in the Windows registry. |
| 172 | + |
| 173 | +- On Linux, little needs to be done because XDG delegates the responsibility to the desktop |
| 174 | + manager. The implementor only needs to create the `.desktop` file and adjust/add the menu XML |
| 175 | + file(s). |
| 176 | +- On macOS, we had to come up with some ideas. The shortcut is actually an `.app` directory. |
| 177 | + Implementors must follow Apple's guidelines. See Addendum B for implementation details. |
| 178 | +- On Windows, `.lnk` files are created with the Windows API. File type and URL protocol association |
| 179 | + is done in the [Windows |
| 180 | + registry](https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types). |
| 181 | + |
| 182 | +Some installations might provide two modes: "Current user only", and "All users". This option is |
| 183 | +not surfaced in the JSON metadata, but might be requested at creation time in the CLI or API. This |
| 184 | +means that implementors MUST be able to handle both user locations and system locations, as |
| 185 | +detailed above. In particular, in-process permission elevation needs to be considered. |
| 186 | + |
| 187 | +When a package is removed, the file artifacts MUST be deleted too. If changes were done in other |
| 188 | +resources (XML files on Linux, Registry on Windows), these MUST be undone too. |
| 189 | + |
| 190 | +## CLI interface |
| 191 | + |
| 192 | +The implementor CLI is _not_ defined in this document. However, the integrations with `constructor` |
| 193 | +SHOULD be standardized if they are going to be kept in use. |
| 194 | + |
| 195 | +The proposed CLI (inspired by what's already in use to introduce minimal changes) is: |
| 196 | + |
| 197 | +```shell |
| 198 | +${IMPLEMENTOR} constructor --prefix ${PREFIX} [--base-prefix ${BASE_PREFIX}] [--mode user|system] [--make-menus | --rm-menus] [pkg-name ...] |
| 199 | +``` |
| 200 | + |
| 201 | +- `--make-menus` will create the menu items for the JSON files found in `$PREFIX/Menu`. |
| 202 | +- `--rm-menus` will uninstall the corresponding menu items from the system. |
| 203 | +- If values are passed next to these two flags, only the JSON files that match those package names |
| 204 | + will be handled. Others will be ignored. |
| 205 | +- `--base-prefix` is optional and defaults to the value passed to `--prefix`. It is only needed |
| 206 | + when `IMPLEMENTOR` is running from a location other than `--prefix` (e.g. `base` vs a custom |
| 207 | + environment, or system Python and a virtual environment). |
| 208 | +- `--mode` is optional and defaults to the mark provided by the `--base-prefix` location. If a |
| 209 | + `.nonadmin` file is present there, `mode=user` will be assumed. Otherwise, `mode=system` will be |
| 210 | + assumed, with a fallback to `mode=user` if necessary. |
| 211 | + |
| 212 | +Alternatively, the `constructor` subcommand needs for menus can be dropped if `IMPLEMENTOR` |
| 213 | +supports new settings and/or CLI flags in the `create | install` commands. Namely: |
| 214 | + |
| 215 | +- `base_prefix`: override the assumed `base` environment location. This is nowadays available as |
| 216 | + `root_prefix` but overriding this with environment variables (via `CONDA_ROOT_PREFIX`) is buggy |
| 217 | + in `conda` and needs to be fixed. |
| 218 | +- Extend `shortcuts` with the ability of accepting values (true, false, or a list of strings). |
| 219 | + - `--shortcuts` would set `shortcuts=True`, which is the default otherwise. |
| 220 | + - `--no-shorcuts` would set `shortcuts=False`. |
| 221 | + - `--shortcuts pkg1 pkg2 ...` would set `shortcuts=[pkg1, pkg2, ...]`, which would instruct |
| 222 | + `IMPLEMENTOR` to handle menu item creation or removal for those packages only. |
| 223 | + |
| 224 | +## Backwards compatibility |
| 225 | + |
| 226 | +Windows users do depend on the existing `menuinst 1.x` "schema". There are a lot of packages that |
| 227 | +use it. This (unversioned) document needs to be kept around and respected. In the absence of the |
| 228 | +`$schema` or `$id` keys, it will be assumed that the metadata is built with the legacy schema. |
| 229 | + |
| 230 | +See Addendum A below for a best effort in documenting it. |
| 231 | + |
| 232 | +## References |
| 233 | + |
| 234 | +* [Rework linux/osx support plus new simplified format?](https://github.com/conda/menuinst/issues/48) |
| 235 | +* [Mamba's implementation in C++](https://github.com/mamba-org/mamba/blob/81a490a/src/core/menuinst.cpp) |
| 236 | +* [Interactions between conda, conda-standalone, constructor and menuinst](https://gist.github.com/jaimergp/7de5843421d63fa4a408ac5c8712c3c9) |
| 237 | +* [Change the API to `menuinst.install(path_or_dict)`](https://github.com/conda/menuinst/issues/25) |
| 238 | +* [`menuinst` wiki as of 2021.10.18](https://github.com/conda/menuinst/wiki/Menu-Shortcut-Config-Structure/632fbc84251c8a8093e1b56b0b5314d23c1e946b) |
| 239 | +* [freedesktop.org specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys) |
| 240 | +* [Core Foundation Keys (info.plist)](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/TP40009249-SW1) |
| 241 | +* [File type association in Windows](https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types) |
| 242 | +* [Default programs in Windows](https://learn.microsoft.com/en-us/windows/win32/shell/default-programs) |
| 243 | + |
| 244 | +## Copyright |
| 245 | + |
| 246 | +All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +## Addendum A |
| 251 | + |
| 252 | +### `menuinst 1.x` pre-standard |
| 253 | + |
| 254 | +The required metadata for each platform is documented in [the `menuinst` wiki][wiki]. However, only |
| 255 | +Windows is really supported by the tool. This assymmetrical growth has allowed Windows to grow an |
| 256 | +ad-hoc specification that doesn't really translate well to other platforms. |
| 257 | + |
| 258 | +The overall schema seems to be: |
| 259 | + |
| 260 | +```python |
| 261 | +{ |
| 262 | + "menu_name": str, |
| 263 | + "menu_items": list of dict, |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +Unfortunately, each menu item dict (let's call it `MenuItem`) takes a different form in each |
| 268 | +platform. |
| 269 | + |
| 270 | +#### `MenuItem` on Windows |
| 271 | + |
| 272 | +```python |
| 273 | +{ |
| 274 | + ["system" | "script" | "pyscript" | "pywscript" | "webbrowser"]: str, |
| 275 | + "scriptargument": str, |
| 276 | + "scriptarguments": list of str, |
| 277 | + "name": str, |
| 278 | + "workdir": str, |
| 279 | + "icon": str, |
| 280 | + "desktop": bool, |
| 281 | + "quicklaunch": bool, |
| 282 | +} |
| 283 | +``` |
| 284 | + |
| 285 | +Currently allowed placeholders are: |
| 286 | + |
| 287 | +* `${PREFIX}`: Python environment prefix |
| 288 | +* `${ROOT_PREFIX}`: Python environment prefix of root (conda or otherwise) installation |
| 289 | +* `${PYTHON_SCRIPTS}`: Scripts folder in Python environment, `${PREFIX}/Scripts` |
| 290 | +* `${MENU_DIR}`: Folder for menu config and icon files, `${PREFIX}/Menu` |
| 291 | +* `${PERSONALDIR}`: Not sure |
| 292 | +* `${USERPROFILE}`: User's home folder |
| 293 | +* `${ENV_NAME}`: The environment in which this shortcut lives. |
| 294 | +* `${DISTRIBUTION_NAME}`: The name of the folder of the root prefix, for example "Miniconda" if |
| 295 | + distribution installed at "C:\Users\Miniconda". |
| 296 | +* `${PY_VER}`: The Python major version only. This is taken from the root prefix. Used generally |
| 297 | + for placing shortcuts under a menu of a parent installation. |
| 298 | +* `${PLATFORM}`: one of (32-bit) or (64-bit). This is taken from the root prefix. Used generally |
| 299 | + for placing shortcuts under a menu of a parent installation. |
| 300 | + |
| 301 | +#### `MenuItem` on MacOS |
| 302 | + |
| 303 | +```python |
| 304 | +{ |
| 305 | + "cmd": str, |
| 306 | + "name": str, |
| 307 | + "icns": str, |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | +Currently allowed placeholders are: |
| 312 | + |
| 313 | +* `${BIN_DIR}`: `PREFIX/bin` |
| 314 | +* `${MENU_DIR}`: `PREFIX/Menu` |
| 315 | + |
| 316 | +#### `MenuItem` on Linux |
| 317 | + |
| 318 | +```python |
| 319 | +{ |
| 320 | + "cmd": list of str, |
| 321 | + "id": str, |
| 322 | + "name": str, |
| 323 | + "comment": str. |
| 324 | + "terminal": bool, |
| 325 | + "icon": str, |
| 326 | +}, |
| 327 | +``` |
| 328 | + |
| 329 | +On Linux, only `cmd` can take two special placeholders `{{FILEBROWSER}}` and `{{WEBBROWSER}}`, |
| 330 | +which are replaced by the default Desktop file explorer, and the default web browser, respectively. |
| 331 | + |
| 332 | +#### Identified problems |
| 333 | + |
| 334 | +##### The `command` interface |
| 335 | + |
| 336 | +Windows has several ways to specify which command should be run with the shortcut: |
| 337 | + |
| 338 | +* `system` + `scriptargument[s]`: path to executable plus its arguments. |
| 339 | +* `script` + `scriptargument[s]`: same as above, but the executable is run in a subprocess after |
| 340 | + invoking `ROOT_PYTHON cwp.py PREFIX`. |
| 341 | +* `pyscript`: hardcodes `script` to be `PREFIX/python.exe` and takes the value as the first (and |
| 342 | + only) argument. |
| 343 | +* `pywscript`: same as above, but uses `pythonw.exe` as the launcher to, theoretically, avoid |
| 344 | + launching a console window next to your application. |
| 345 | +* `webbrowser`: alias to `PREFIX/python -m webbrowser -t URL`. |
| 346 | + |
| 347 | +On Linux the command is specified with `cmd`, expressed as a list of strings. On MacOS, `cmd` is |
| 348 | +also taken, but in this case it's expected to be a raw string. |
| 349 | + |
| 350 | +##### The `icon` key |
| 351 | + |
| 352 | +Windows and Linux expect `icon`. MacOS expects `icns`. Each platform requires a different file |
| 353 | +format, but that can be arranged with placeholders. |
| 354 | + |
| 355 | +##### Standardize the placeholders |
| 356 | + |
| 357 | +Allowed placeholders vary vastly across platforms. A common subset must be identified and |
| 358 | +implemented. Per-platform options are allowed but only when strictly necessary. |
| 359 | + |
| 360 | +## Addendum B: Implementation details in macOS |
| 361 | + |
| 362 | +- Most of the macOS-specific settings map to the `.app`'s `Info.plist` key-value pairs. |
| 363 | +- The shell script with the `precommand` + `activate` + `command` logic is located in |
| 364 | + `<NAME>.app/Contents/MacOS/<NAME>-script`. |
| 365 | +- A binary launcher is required for correct system integration (see reasons |
| 366 | + [`conda/menuinst#123`](https://github.com/conda/menuinst/issues/123)). This is placed at |
| 367 | + `<NAME>.app/Contents/MacOS/<NAME>`. The proposed launcher simply guesses its own location to find |
| 368 | + the `*-script` file, which is spawned in a subprocess. |
| 369 | +- In some cases, if an external binary is required, it needs to be symlinked into the `.app` |
| 370 | + directory to ensure keyboard integrations work (see |
| 371 | + [`conda/menuinst#122`](https://github.com/conda/menuinst/issues/122)). |
| 372 | +- URL protocol association requires special support in the binary launcher. Implementors can choose |
| 373 | + how to implement it. [See this issue](https://github.com/conda/menuinst/issues/118) and |
| 374 | + [this PR](https://github.com/conda/menuinst/pull/119) for ideas. |
| 375 | + |
| 376 | +<!--links--> |
| 377 | +[wiki]: |
| 378 | + https://github.com/conda/menuinst/wiki/Menu-Shortcut-Config-Structure/632fbc84251c8a8093e1b56b0b5314d23c1e946b |
| 379 | + |
| 380 | +[menuinst-json-schema]: https://github.com/conda/menuinst/blob/cep-devel/menuinst/data/menuinst.schema.json |
| 381 | + |
| 382 | +[RFC2119]: https://datatracker.ietf.org/doc/html/rfc2119 |
0 commit comments