Skip to content

Commit 3da0fb0

Browse files
authored
Prepare menuinst for multiplatform support (#8)
* Formalize schema for menuinst * use this cep to discuss other menuinst issues * more refs * Add proposed schema * update with latest changes and ideas * update with latest ideasva * merged * Simplify proposal * Propose alternative to `... constructor --make-menus` * one more comment * update header * a couple notes more * add better references to known issues and workarounds in macos * update with latest fields * typos * adopt RFC2119 * update header tbale * amend JSON filename guidelines * Accept * mint * remint
1 parent 8aac9d6 commit 3da0fb0

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed

cep-11.md

+382
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
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 &lt;[email protected]&gt;</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

Comments
 (0)