========================
Workspace export support
========================
Anaconda Project ships tooling aimed at converting existing
``anaconda-project.yml`` projects into forms that can be managed by
`pixi `_ or by
`conda-workspaces `_.
Both are independently-writable export targets: ``export-pixi``
writes a ``pixi.toml``, and ``export-conda`` writes a ``conda.toml``.
The two formats share almost all of their TOML shape — the same
``[workspace]``, ``[dependencies]``, ``[feature.*]``,
``[environments]``, and ``[tasks]`` tables — so a single conversion
pipeline backs both commands; ``conda.toml`` is not merely something
this repository reads because it happens to also consume
``pixi.toml`` files, it is a first-class output of its own.
These are the focus of recent maintenance on this repository:
* The ``anaconda-project export-pixi`` command, which writes a
``pixi.toml`` (and a sibling ``ap_download.py`` helper when needed)
alongside an existing ``anaconda-project.yml``.
* The ``anaconda-project export-conda`` command, which writes a
``conda.toml`` (and the same sibling ``ap_download.py`` helper when
needed) alongside an existing ``anaconda-project.yml``, using the
identical conversion rules as ``export-pixi``.
* A ``publication_info`` function in
``anaconda_project.project_info`` that can read
``anaconda-project.yml``, ``pixi.toml``, ``conda.toml``, or a
``pyproject.toml`` embedding either format (``[tool.pixi]`` /
``[tool.conda]``) and return a uniform dictionary describing the
project's commands, environments, and variables. Downstream
deployment tooling that ingests any of these formats uses this as
its single integration point.
* The ``anaconda-project info`` command, a human-readable view of the
same data — modeled on ``pixi info`` — that works on any of the
supported manifest formats.
The rest of this page documents the conventions the conversion uses
to preserve as much of the original project's behavior as possible.
Except where noted, everything below applies equally to
``export-pixi``/``pixi.toml`` and ``export-conda``/``conda.toml`` — the
two exporters produce identical content for the same project and
flags.
Export goals
============
The conversion is best-effort but tries to satisfy three contracts:
* The resulting ``pixi.toml`` should be installable and runnable
without further manual editing for the common project shapes
(``unix:`` commands, ``notebook:``, ``bokeh_app:``, ``downloads:``,
``variables:``, single or multi-environment).
* Tasks invoked under pixi should mirror the runtime behavior of
``anaconda-project run`` as closely as the underlying tools allow,
including HTTP options, environment selection, and download fetching.
* The converted manifest should be readable for a maintainer who
inspects it later — comments mark anything that couldn't be
translated faithfully, and warnings appear at the top of the file
when a downstream consumer needs to act.
Environment layout
==================
* When the source yml has a single ``env_specs:`` entry with a
non-default name (for example ``sampleproj:``), packages live in
the top-level ``[dependencies]`` table (pixi's default feature) and
the named env inherits via ``features = ["sampleproj"]``. Pixi's
mandatory implicit ``default`` env carries the same packages, so
``pixi install`` (without ``-e``) still does something useful.
* Multi-environment projects emit each ``env_specs:`` entry under
``[environments]`` in source order. The first uncommented entry is
the project's intended default; downstream tools can extract it with
one line of ``awk``.
* If one of multiple env_specs is literally named ``default``, its
``[environments]`` slot is rendered as a comment
(``# default (pixi creates this implicitly...)``) and its packages
are folded into top-level ``[dependencies]``. Pixi auto-creates
``default`` from the default feature, so redeclaring it would be
redundant.
* ``solve-group`` is intentionally not emitted. Anaconda Project does
not assume environments solve together, and pixi shouldn't be told
otherwise on import.
The ``--use-default`` flag
--------------------------
Projects that have no env_spec literally named ``default`` route every
dependency, task, and prepare body through ``[feature.{name}.*]``
indirection — clean for projects that need explicit fan-out, but verbose
for the common case of a single env (or a default command bound to one
specific env in a multi-env project). Passing ``--use-default`` to
``export-pixi`` collapses that:
* The exporter picks one env_spec to promote to ``default``: the env_spec
attached to the project's default command (the one a bare
``anaconda-project run`` would invoke), or — if there are no commands —
the first ``env_specs:`` entry declared.
* That env_spec's packages, tasks, and prepare body land in top-level
``[dependencies]`` / ``[tasks.X]`` / ``[tasks.prepare]`` instead of
inside ``[feature.{name}.*]`` blocks. Other env_specs (in a multi-env
project) keep their ``[feature.{name}.*]`` scoping unchanged.
* The flag is a no-op when an env_spec literally named ``default``
already exists — there's nothing to rename.
* Under ``--use-default`` the no-op ``prepare`` task is dropped. When
there are no ``downloads:`` to fetch, the unflagged export emits an
``echo`` placeholder in ``[feature.{name}.tasks.prepare]`` so
``pixi run prepare`` resolves to the project's intended env even
when that env's name isn't ``default``. Under ``--use-default`` the
promoted env *is* the implicit default, so the placeholder loses
its purpose; ``prepare`` is only emitted when there's real work to
do.
When the user runs ``export-pixi`` without the flag and the project would
benefit, the CLI prints a recommendation naming the env that would be
promoted. With the flag on, the CLI confirms which env was renamed.
The renaming applies only to the exported ``pixi.toml``; the source
``anaconda-project.yml`` is not modified.
The ``--add-current-platform`` flag
-----------------------------------
Pixi rejects an env that doesn't list the host platform; anaconda-project
is more forgiving, so it's common to find a ported project with a
``platforms:`` list that worked under conda but breaks at pixi install
time on the developer's machine. Passing ``--add-current-platform`` to
``export-pixi`` widens the converted manifest's ``platforms`` list to
include the host's conda subdir (e.g. ``osx-arm64``) when it isn't
already declared.
Behavior mirrors ``--use-default``:
* Off by default. The exporter does not silently mutate the user's
platforms list — the user explicitly chose what to support.
* When omitted, the CLI prints a recommendation if the host platform
is missing.
* When passed, the CLI prints a confirmation listing the platform
that was added — or stays quiet when the platform was already
present.
The change applies only to the exported ``pixi.toml``; the source
``anaconda-project.yml`` is not modified.
Channel handling
================
Pixi has no ``defaults`` meta-channel and no equivalent of conda's
``default_channels`` configuration. Two cases are handled explicitly:
* If the source yml lists no channels at all, the converted manifest
is populated from ``conda config --show default_channels`` rather
than a hard-coded ``conda-forge`` fallback. This preserves
enterprise users' ``.condarc``-configured mirrors.
* If the source yml includes ``defaults`` alongside other channels,
only the ``defaults`` entry is expanded; the rest are preserved in
source order with duplicate URLs removed.
If ``conda`` is not on PATH (or fails to invoke) and ``defaults``
expansion is required, the conversion fails fast — no partial output
is written.
Tasks and command translation
=============================
* ``unix:`` command lines are translated to a form compatible with
pixi's ``deno_task_shell`` task runner. ``${VAR}`` and ``%VAR%``
references are normalized to ``$VAR``; ``${PROJECT_DIR}`` becomes
``$PIXI_PROJECT_ROOT``.
* When both ``unix:`` and ``windows:`` command lines are present, the
converter normalizes the windows form (path separators, env vars)
and compares to the unix form. Matching forms collapse to a single
task; divergent forms emit the unix variant plus a comment noting
what the windows form would have rendered.
* ``${CONDA_PREFIX}/bin/``, ``${CONDA_PREFIX}/Scripts/``,
and similar prefix-rooted paths are stripped to the bare command
name. Pixi's task activation already prepends the env's executable
directories to ``PATH``, so explicit prefix paths are redundant and
hurt cross-platform portability.
The ``prepare`` task
====================
Every converted project gets a ``prepare`` task. It does double duty:
* When the source yml declared ``downloads:``, ``prepare`` runs a
helper script (``ap_download.py``, written next to ``pixi.toml``)
once per download. The helper is pure stdlib and uses ``python3``,
so it works whether or not the env declares its own python.
* Even when there are no downloads, ``prepare`` is emitted as a no-op
``echo``. Its presence serves two purposes:
- Acts as a marker that downstream deployment tooling can use to
detect that this ``pixi.toml`` was converted from
``anaconda-project.yml``.
- When scoped to a non-default env's feature (e.g.
``[feature.sampleproj.tasks.prepare]``), forces
``pixi run prepare`` to resolve to that env automatically — useful
when the project's default env_spec is named something other than
``default``.
If a download-needing env doesn't declare ``python``, the converted
``pixi.toml`` carries a ``# WARNING:`` comment block at the top
listing the affected envs, and the same warning is printed to stderr
at conversion time. The conversion does not silently mutate the
user's package list.
HTTP options
============
Anaconda Project's ``supports_http_options: true`` (implicit on
``notebook:`` and ``bokeh_app:`` commands) tells the underlying tool
to expect ``--anaconda-project-X`` flags for host, port, address,
url-prefix, iframe-hosts, no-browser, and use-xheaders. The exporter
translates this contract into pixi ``args`` and templated ``cmd``
strings, dispatching by command type:
* ``notebook:`` commands become ``jupyter notebook `` plus
Jupyter-native flags (``--port``, ``--ip``,
``--NotebookApp.base_url=...``,
``--NotebookApp.tornado_settings={...}`` for iframe hosts,
``--no-browser``, ``--NotebookApp.trust_xheaders=True``). ``host``
is dropped because Jupyter has no host-restrict equivalent.
* ``bokeh_app:`` commands become ``bokeh serve `` plus bokeh's
bare flags (``--host``, ``--port``, ``--address``, ``--prefix``,
``--use-xheaders``). ``--show`` is rendered as the *inverse* of
``--no-browser``. ``iframe_hosts`` is dropped because bokeh has no
Content-Security-Policy equivalent.
* Plain ``unix:`` commands with ``supports_http_options: true`` keep
their ``--anaconda-project-X`` flags verbatim. Generic tools that
opt in are expected to recognize the canonical names themselves
(``panel serve`` does).
* ``unix:`` commands with ``supports_http_options: false`` are
scanned for HTTP Jinja vars (``{{ port }}``, ``{{ host }}``, etc.)
in their templates; pixi ``args`` are declared only for the vars
the cmd actually references.
Each flag is wrapped in a Jinja conditional, so a blank pixi arg
omits the flag entirely (rather than passing an empty value the
underlying tool might reject).
The ``{% if var %}...{% endif %}`` and ``{{ var }}`` template syntax
inside task ``cmd`` strings is intentional, not accidental — pixi
renders task commands through MiniJinja against the positional values
passed to ``pixi run `` (with ``args`` defaults
filling in the rest), and only then hands the rendered command to
``deno_task_shell`` for execution. The syntax is supported but not
prominently documented in pixi's own docs; the converted ``pixi.toml``
is exercising a real pixi feature, and editing the gates by hand is
fine.
The ``info`` command
====================
``anaconda-project info`` prints a human-readable summary of the
project, modeled on ``pixi info``::
$ anaconda-project info --directory .
Project
------------
Type: pixi
Name: Attractors
Description: A panel dashboard using datashader ...
Directory: /path/to/project
Commands
------------
Command: dashboard [default, http]
: env_spec: sampleproj
: cmd: panel serve attractors.ipynb {% if host %}...
: args: host, port, address, url_prefix, ...
Environments
------------
Environment: default
Channels: https://repo.anaconda.com/pkgs/main, ...
Dependency count: 12
Dependencies: colorcet, datashader, fiona, geoviews, ...
Locked: yes
The command works on any supported manifest format. When no format is
forced, detection follows this precedence: ``conda.toml``, then
``pixi.toml``, then ``anaconda-project.yml`` (or its ``.yaml`` /
``kapsel.*`` aliases), then ``pyproject.toml`` (checking
``[tool.conda.workspace]`` before ``[tool.pixi.workspace]``).
Flags:
* ``--json`` emits the underlying ``publication_info`` dict as
indented JSON, mirroring ``pixi info --json``.
* ``--env-paths`` includes the on-disk prefix path for each
environment. For pixi projects (top-level ``pixi.toml`` or a
``pyproject.toml`` ``[tool.pixi]`` embedding) this shells out to
``pixi info --json``; the full pixi payload is also stashed
under an ``_pixi`` key in ``--json`` mode so callers don't have
to repeat the subprocess. conda-workspaces projects have no
equivalent path-resolution mechanism yet, so this flag has no
effect for them.
* ``--project-type {conda-workspaces,pixi,anaconda-project}`` forces
a manifest format when more than one is present in the directory
(e.g. mid-conversion). Forcing a type recognizes that format
whether it is a top-level manifest (``conda.toml`` or ``pixi.toml``)
or a ``pyproject.toml`` embedding (``[tool.conda.workspace]`` or
``[tool.pixi.workspace]``), and it is checked independently of
auto-detect precedence — so a coexisting higher-precedence manifest
of a *different* format never masks the one you asked for. It is an
error only when the requested format is present in neither form.
publication_info
================
``anaconda_project.project_info.publication_info(project_dir)``
returns a uniform dictionary regardless of whether the project is
managed by ``anaconda-project.yml``, a converted ``pixi.toml``, a
converted ``conda.toml``, or a ``pyproject.toml`` embedding either
format under ``[tool.pixi]`` / ``[tool.conda]``. The shape mirrors
``Project.publication_info()`` from the anaconda-project side;
relevant additions for the pixi and conda-workspaces sides (field
population is identical between the two — the only differences are
the manifest filename and lockfile filename each reads):
* ``commands[name]['args']`` — ordered list of task arg names
declared for the task. Empty for tasks without args. Lets
downstream tooling drive ``pixi run *args`` (or the
conda-workspaces equivalent) to supply positional values.
* ``commands[name]['env_spec']`` — resolves to the name of an env
that supports the task. Top-level tasks resolve to the project's
default env (literal ``default`` if declared, otherwise the first
declared env). Feature-scoped tasks resolve to whichever env
actually includes the feature.
* ``env_specs[name]['locked']`` — ``True`` when the sibling lockfile
(``pixi.lock`` or ``conda.lock``) has an ``environments[]``
entry. Best-effort: any read or parse failure silently falls back
to ``False``.
For the ``pyproject.toml``-embedded case, ``name``/``description``
resolution has one documented gap: upstream conda-workspaces itself
falls back to PEP 621 ``[project].name`` / ``[project].description``
when ``[tool.conda.workspace]`` (or ``[tool.pixi.workspace]``) omits
those fields. This plan's read-side summary does not replicate that
PEP 621 fallback — if ``workspace.name`` is absent, the directory
basename is used instead, matching the top-level-manifest behavior.
This is a deliberate scope limitation (a uniform read-side summary,
not full-fidelity mirroring of every upstream fallback path), not a
bug.
Optional arguments:
* ``project_type='conda-workspaces' | 'pixi' | 'anaconda-project'``
forces a specific manifest format. Default behavior is auto-detect,
with ``conda.toml`` winning over ``pixi.toml``, which wins over
``anaconda-project.yml`` (or its aliases), which wins over a
``pyproject.toml`` embedding. Forcing a type recognizes that format
whether it is the top-level manifest or a ``pyproject.toml``
embedding, checked independently of that precedence order, so a
coexisting higher-precedence manifest of a *different* format never
masks it. It is an error only when the requested format is present
in neither form in the directory.
* ``env_paths=True`` populates ``env_specs[name]['path']`` with the
on-disk prefix for each declared environment. For anaconda-project
this is derived from each ``EnvSpec``; for pixi (top-level or
pyproject.toml-embedded) it requires a ``pixi info --json``
subprocess (so it is opt-in). Failure to invoke pixi or parse its
output raises ``RuntimeError``. When the source is pixi, the full
``pixi info --json`` payload is also stored under the top-level
``_pixi`` key so callers that want richer pixi-specific data don't
pay for a second subprocess. conda-workspaces projects have no
equivalent mechanism yet, so ``env_paths=True`` has no effect on
conda-workspaces results.
Programmatic export API
=======================
For tools that wrap the conversion (launchers, IDE plugins, custom CLIs),
the export pipeline is exposed so downstream code never has to scrape
generated TOML or re-implement the conversion rules.
* ``project_ops.export_pixi(project, filename, use_default=False,
add_current_platform=False)`` writes ``pixi.toml`` to disk and returns
a ``PixiExportStatus``. On success the status carries:
- ``default_rename_from`` — the env_spec promoted to ``default`` by
``use_default``, or ``None`` when the flag was off, no-op'd
(because ``default`` already existed), or the export failed.
- ``current_platform_added`` — the platform string added to the
``platforms`` list by ``add_current_platform``, or ``None`` when
the flag was off, no-op'd (the platform was already declared), or
the export failed.
Callers use these to surface "Renamed env X → default" or "Added
platform Y" in their success notification without re-querying the
project.
* ``project_ops.preview_pixi_export(project, use_default=False,
add_current_platform=False)`` runs the conversion in memory and
returns a dict
``{pixi_toml, default_rename_from, current_platform_addition_target,
warnings}``. The ``pixi_toml`` entry is the would-be file content;
the rename / platform-addition fields report the *candidates*
(whether or not their flag was passed), so a confirmation dialog
can offer "re-render with --use-default" or "re-render with
--add-current-platform" as actions; ``warnings`` is the leading
``# WARNING:`` block already extracted from the rendered TOML, so
callers don't have to grep for it themselves. Raises
``CondaNotAvailableError`` if the conversion needs ``conda config
--show default_channels`` and conda isn't reachable.
* ``project_ops.export_conda(project, filename, use_default=False,
add_current_platform=False)`` writes ``conda.toml`` to disk and
returns a ``PixiExportStatus`` — the exact same status class
``export_pixi`` returns, since ``default_rename_from`` and
``current_platform_added`` are generic export concepts with nothing
pixi-specific baked into the class. Semantics of both fields are
identical to ``export_pixi``'s.
* ``project_ops.preview_conda_export(project, use_default=False,
add_current_platform=False)`` runs the conversion in memory and
returns a dict
``{conda_toml, default_rename_from, current_platform_addition_target,
warnings}`` — the same four concepts as ``preview_pixi_export``,
with only the file-content key renamed (``conda_toml`` instead of
``pixi_toml``) to match the format it describes.
* ``anaconda_project.internal.pixi_export.export_conda_toml(project,
use_default=False, add_current_platform=False,
default_channels=None)`` is the module-level function
``export_conda`` calls to produce ``conda.toml`` content; it
delegates directly to ``export_pixi_toml`` and returns its result
unchanged. conda-workspaces 0.7.0's TOML shape is field-for-field
identical to pixi.toml's for every field this exporter emits, so no
translation step exists between the two — the same function
produces both outputs, distinguished only by which file the caller
writes it to.
* ``anaconda_project.internal.pixi_export.default_rename_target(project)``
returns the env_spec name that ``--use-default`` would promote, or
``None`` when promotion is a no-op (project already has a
``default`` env_spec, or has no env_specs at all). Stable contract;
the underlying selection rule (default-command's env, else first
declared env_spec) lives in this one function. Shared by both the
pixi and conda-workspaces export paths.
* ``anaconda_project.internal.pixi_export.current_platform_addition_target(project)``
returns the host's conda subdir if it would be added by
``--add-current-platform``, or ``None`` when it's already in the
union of declared platforms. Same shape as
``default_rename_target`` so callers can decide both flags
symmetrically. Shared by both the pixi and conda-workspaces export
paths.
Limitations
===========
A few aspects of ``anaconda-project.yml`` cannot be translated
faithfully. The exporter surfaces them as comments rather than
silently dropping them:
* ``services:`` (e.g. Redis) — pixi has no equivalent service-launch
primitive. The converted manifest carries a comment listing the
required services so a maintainer can wire them up out of band.
* Variables without defaults — anaconda-project would prompt; pixi
cannot. The converted manifest emits a comment listing variables
that must be set in the environment before running.