diff --git a/Cargo.lock b/Cargo.lock index f506ded2a..937a067c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ dependencies = [ "self-replace", "serde", "serde-untagged", + "serde_ignored", "serde_json", "serde_with", "serde_yaml", @@ -4600,6 +4601,15 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "serde_ignored" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e319a36d1b52126a0d608f24e93b2d81297091818cd70625fcf50a15d84ddf" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.116" diff --git a/Cargo.toml b/Cargo.toml index 06506c7fb..c3c3986ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ reqwest-retry = "0.5.0" self-replace = "1.3.7" serde = "1.0.198" serde-untagged = "0.1.5" +serde_ignored = "0.1.10" serde_json = "1.0.116" serde_with = { version = "3.7.0", features = ["indexmap"] } serde_yaml = "0.9.34" diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 6ba807f8b..79e41d627 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -102,7 +102,7 @@ The JSON should follow the following format: Note: if you use a wildcard in the host, any subdomain will match (e.g. `*.prefix.dev` also matches `repo.prefix.dev`). -Lastly you can set the authentication override file in the [global configuration file](./global_configuration.md). +Lastly you can set the authentication override file in the [global configuration file](./../reference/pixi_configuration.md). ## PyPI authentication Currently, we support the following methods for authenticating against PyPI: @@ -122,7 +122,7 @@ To enable this use the CLI flag `--pypi-keyring-provider` which can either be se pixi install --pypi-keyring-provider subprocess ``` -This option can also be set in the global configuration file under [pypi-config](./global_configuration.md#pypi-configuration). +This option can also be set in the global configuration file under [pypi-config](./../reference/pixi_configuration.md#pypi-configuration). #### Installing keyring To install keyring you can use pixi global install: @@ -191,7 +191,7 @@ gcloud artifacts print-settings python --project= --repository=`: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--host`: Specifies a host dependency, important for building a package. - `--build`: Specifies a build dependency, important for building a package. - `--pypi`: Specifies a PyPI dependency, not a conda package. Parses dependencies as [PEP508](https://peps.python.org/pep-0508/) requirements, supporting extras and versions. - See [configuration](configuration.md) for details. + See [configuration](project_configuration.md) for details. - `--no-install`: Don't install the package to the environment, only add the package to the lock-file. - `--no-lockfile-update`: Don't update the lock-file, implies the `--no-install` flag. - `--platform (-p)`: The platform for which the dependency should be added. (Allowed to be used more than once) @@ -95,8 +95,8 @@ pixi add --feature featurex numpy ## `install` -Installs an environment based on the [manifest file](configuration.md). -If there is no `pixi.lock` file or it is not up-to-date with the [manifest file](configuration.md), it will (re-)generate the lock file. +Installs an environment based on the [manifest file](project_configuration.md). +If there is no `pixi.lock` file or it is not up-to-date with the [manifest file](project_configuration.md), it will (re-)generate the lock file. `pixi install` only installs one environment at a time, if you have multiple environments you can select the right one with the `--environment` flag. If you don't provide an environment, the `default` environment will be installed. @@ -106,9 +106,9 @@ As all commands interacting with the environment will first run the `install` co E.g. `pixi run`, `pixi shell`, `pixi shell-hook`, `pixi add`, `pixi remove` to name a few. ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--environment (-e)`: The environment to install, if none are provided the default environment will be used. ```shell @@ -136,7 +136,7 @@ This command will allow you to update the lock file directly, without manually d The `run` commands first checks if the environment is ready to use. When you didn't run `pixi install` the run command will do that for you. -The custom tasks defined in the [manifest file](configuration.md) are also available through the run command. +The custom tasks defined in the [manifest file](project_configuration.md) are also available through the run command. You cannot run `pixi run source setup.bash` as `source` is not available in the `deno_task_shell` commandos and not an executable. @@ -146,9 +146,9 @@ You cannot run `pixi run source setup.bash` as `source` is not available in the ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--environment (-e)`: The environment to run the task in, if none are provided the default environment will be used or a selector will be given to select the right environment. ```shell @@ -196,7 +196,7 @@ pixi run --environment cuda python ## `remove` -Removes dependencies from the [manifest file](configuration.md). +Removes dependencies from the [manifest file](project_configuration.md). If the project manifest is a `pyproject.toml`, removing a pypi dependency with the `--pypi` flag will remove it from either - the native pyproject `project.dependencies` array or the native `project.optional-dependencies` table (if a feature is specified) @@ -208,7 +208,7 @@ If the project manifest is a `pyproject.toml`, removing a pypi dependency with t ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--host`: Specifies a host dependency, important for building a package. - `--build`: Specifies a build dependency, important for building a package. - `--pypi`: Specifies a PyPI dependency, not a conda package. @@ -236,11 +236,11 @@ If you want to make a shorthand for a specific command you can add a task for it ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. ### `task add` -Add a task to the [manifest file](configuration.md), use `--depends-on` to add tasks you want to run before this task, e.g. build before an execute task. +Add a task to the [manifest file](project_configuration.md), use `--depends-on` to add tasks you want to run before this task, e.g. build before an execute task. ##### Arguments @@ -265,7 +265,7 @@ pixi task add build-osx "METAL=1 cargo build" --platform osx-64 pixi task add train python train.py --feature cuda ``` -This adds the following to the [manifest file](configuration.md): +This adds the following to the [manifest file](project_configuration.md): ```toml [tasks] @@ -290,7 +290,7 @@ pixi run test --test test1 ### `task remove` -Remove the task from the [manifest file](configuration.md) +Remove the task from the [manifest file](project_configuration.md) ##### Arguments @@ -351,11 +351,11 @@ List project's packages. Highlighted packages are explicit dependencies. - `--json`: Whether to output in json format. - `--json-pretty`: Whether to output in pretty json format - `--sort-by `: Sorting strategy [default: name] [possible values: size, name, type] -- `--explicit (-x)`: Only list the packages that are explicitly added to the [manifest file](configuration.md). -- `--manifest-path `: The path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--explicit (-x)`: Only list the packages that are explicitly added to the [manifest file](project_configuration.md). +- `--manifest-path `: The path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--environment (-e)`: The environment's packages to list, if non is provided the default environment's packages will be listed. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) ```shell @@ -370,7 +370,7 @@ pixi list --locked pixi list --no-install ``` -Output will look like this, where `python` will be green as it is the package that was explicitly added to the [manifest file](configuration.md): +Output will look like this, where `python` will be green as it is the package that was explicitly added to the [manifest file](project_configuration.md): ```shell ➜ pixi list @@ -412,10 +412,10 @@ The package tree can also be inverted (`-i`), to see which packages require a sp - `--invert (-i)`: Invert the dependency tree, that is given a `REGEX` pattern that matches some packages, show all the packages that depend on those. - `--platform (-p)`: The platform to list packages for. Defaults to the current platform -- `--manifest-path `: The path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: The path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--environment (-e)`: The environment's packages to list, if non is provided the default environment's packages will be listed. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) ```shell @@ -429,7 +429,7 @@ pixi tree --platform win-64 !!! warning Use `-v` to show which `pypi` packages are not yet parsed correctly. The `extras` and `markers` parsing is still under development. -Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. +Output will look like this, where direct packages in the [manifest file](project_configuration.md) will be green. Once a package has been displayed once, the tree won't continue to recurse through its dependencies (compare the first time `python` appears, vs the rest), and it will instead be marked with a star `(*)`. Version numbers are colored by the package type, yellow for Conda packages and blue for PyPI. @@ -523,9 +523,9 @@ To exit the pixi shell, simply run `exit`. ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--environment (-e)`: The environment to activate the shell in, if none are provided the default environment will be used or a selector will be given to select the right environment. ```shell @@ -549,9 +549,9 @@ This command prints the activation script of an environment. - `--shell (-s)`: The shell for which the activation script should be printed. Defaults to the current shell. Currently supported variants: [`bash`, `zsh`, `xonsh`, `cmd`, `powershell`, `fish`, `nushell`] -- `--manifest-path`: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). -- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--manifest-path`: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lock file, doesn't update `pixi.lock` if it isn't up-to-date with [manifest file](project_configuration.md). It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: only install if the `pixi.lock` is up-to-date with the [manifest file](project_configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--environment (-e)`: The environment to activate, if none are provided the default environment will be used or a selector will be given to select the right environment. - `--json`: Print all environment variables that are exported by running the activation script as JSON. When specifying this option, `--shell` is ignored. @@ -585,7 +585,7 @@ Search a package, output will list the latest version of the package. ###### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--channel (-c)`: specify a channel that the project uses. Defaults to `conda-forge`. (Allowed to be used more than once) - `--limit (-l)`: optionally limit the number of search results - `--platform (-p)`: specify a platform that you want to search for. (default: current platform) @@ -621,7 +621,7 @@ More information [here](../advanced/explain_info_command.md). ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. - `--extended`: extend the information with more slow queries to the system, like directory sizes. - `--json`: Get a machine-readable version of the information as output. @@ -695,7 +695,7 @@ Use this command to manage the configuration. - `--global`: Specify management scope to global configuration. - `--local`: Specify management scope to local configuration. -Checkout the [global configuration](../advanced/global_configuration.md) for more information about the locations. +Checkout the [pixi configuration](./pixi_configuration.md) for more information about the locations. ### `config edit` @@ -758,6 +758,8 @@ Set a configuration key to a value. pixi config set default-channels '["conda-forge", "bioconda"]' pixi config set --global mirrors '{"https://conda.anaconda.org/": ["https://prefix.dev/conda-forge"]}' pixi config set repodata-config.disable-zstd true --system +pixi config set --global detached-environments "/opt/pixi/envs" +pixi config set detached-environments false ``` ### `config unset` @@ -902,7 +904,7 @@ This subcommand allows you to modify the project configuration through the comma ##### Options -- `--manifest-path `: the path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--manifest-path `: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories. ### `project channel add` diff --git a/docs/reference/pixi_configuration.md b/docs/reference/pixi_configuration.md new file mode 100644 index 000000000..0f2c3053c --- /dev/null +++ b/docs/reference/pixi_configuration.md @@ -0,0 +1,243 @@ +# The configuration of pixi itself + +Apart from the [project specific configuration](../reference/project_configuration.md) pixi supports configuration options which are not required for the project to work but are local to the machine. +The configuration is loaded in the following order: + + +=== "Linux" + + | **Priority** | **Location** | **Comments** | + |--------------|------------------------------------------------------------------------|------------------------------------------------------------------------------------| + | 1 | `/etc/pixi/config.toml` | System-wide configuration | + | 2 | `$XDG_CONFIG_HOME/pixi/config.toml` | XDG compliant user-specific configuration | + | 3 | `$HOME/.config/pixi/config.toml` | User-specific configuration | + | 4 | `$PIXI_HOME/config.toml` | Global configuration in the user home directory. `PIXI_HOME` defaults to `~/.pixi` | + | 5 | `your_project/.pixi/config.toml` | Project-specific configuration | + | 6 | Command line arguments (`--tls-no-verify`, `--change-ps1=false`, etc.) | Configuration via command line arguments | + +=== "macOS" + + | **Priority** | **Location** | **Comments** | + |--------------|------------------------------------------------------------------------|------------------------------------------------------------------------------------| + | 1 | `/etc/pixi/config.toml` | System-wide configuration | + | 2 | `$XDG_CONFIG_HOME/pixi/config.toml` | XDG compliant user-specific configuration | + | 3 | `$HOME/Library/Application Support/pixi/config.toml` | User-specific configuration | + | 4 | `$PIXI_HOME/config.toml` | Global configuration in the user home directory. `PIXI_HOME` defaults to `~/.pixi` | + | 5 | `your_project/.pixi/config.toml` | Project-specific configuration | + | 6 | Command line arguments (`--tls-no-verify`, `--change-ps1=false`, etc.) | Configuration via command line arguments | + +=== "Windows" + + | **Priority** | **Location** | **Comments** | + |--------------|------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| + | 1 | `C:\ProgramData\pixi\config.toml` | System-wide configuration | + | 2 | `%APPDATA%\pixi\config.toml` | User-specific configuration | + | 3 | `$PIXI_HOME\config.toml` | Global configuration in the user home directory. `PIXI_HOME` defaults to `%USERPROFILE%/.pixi` | + | 4 | `your_project\.pixi\config.toml` | Project-specific configuration | + | 5 | Command line arguments (`--tls-no-verify`, `--change-ps1=false`, etc.) | Configuration via command line arguments | + +!!! note + The highest priority wins. If a configuration file is found in a higher priority location, the values from the configuration read from lower priority locations are overwritten. + + +!!! note + To find the locations where `pixi` looks for configuration files, run + `pixi` with `-v` or `--verbose`. + +## Reference + +??? info "Casing In Configuration" + In versions of pixi `0.20.1` and older the global configuration used snake_case + we've changed to `kebab-case` for consistency with the rest of the configuration. + But we still support the old `snake_case` configuration, for older configuration options. + These are: + + - `default_channels` + - `change_ps1` + - `tls_no_verify` + - `authentication_override_file` + - `mirrors` and sub-options + - `repodata-config` and sub-options + +The following reference describes all available configuration options. + +### `default-channels` + +The default channels to select when running `pixi init` or `pixi global install`. +This defaults to only conda-forge. +```toml title="config.toml" +default-channels = ["conda-forge"] +``` +!!! note + The `default-channels` are only used when initializing a new project. Once initialized the `channels` are used from the project manifest. + +### `change-ps1` + +When set to false, the `(pixi)` prefix in the shell prompt is removed. +This applies to the `pixi shell` subcommand. +You can override this from the CLI with `--change-ps1`. + +```toml title="config.toml" +change-ps1 = true +``` + +### `tls-no-verify` +When set to true, the TLS certificates are not verified. + +!!! warning + + This is a security risk and should only be used for testing purposes or internal networks. + +You can override this from the CLI with `--tls-no-verify`. + +```toml title="config.toml" +tls-no-verify = false +``` + +### `authentication-override-file` +Override from where the authentication information is loaded. +Usually, we try to use the keyring to load authentication data from, and only use a JSON +file as a fallback. This option allows you to force the use of a JSON file. +Read more in the authentication section. +```toml title="config.toml" +authentication-override-file = "/path/to/your/override.json" +``` + +### `detached-environments` +The directory where pixi stores the project environments, what would normally be placed in the `.pixi/envs` folder in a project's root. +It doesn't affect the environments built for `pixi global`. +The location of environments created for a `pixi global` installation can be controlled using the `PIXI_HOME` environment variable. +!!! warning + We recommend against using this because any environment created for a project is no longer placed in the same folder as the project. + This creates a disconnect between the project and its environments and manual cleanup of the environments is required when deleting the project. + + However, in some cases, this option can still be very useful, for instance to: + + - force the installation on a specific filesystem/drive. + - install environments locally but keep the project on a network drive. + - let a system-administrator have more control over all environments on a system. + +This field can consist of two types of input. + +- A boolean value, `true` or `false`, which will enable or disable the feature respectively. (not `"true"` or `"false"`, this is read as `false`) +- A string value, which will be the absolute path to the directory where the environments will be stored. + +```toml title="config.toml" +detached-environments = true +``` +or: +```toml title="config.toml" +detached-environments = "/opt/pixi/envs" +``` + +The environments will be stored in the [cache directory](../features/environment.md#caching) when this option is `true`. +When you specify a custom path the environments will be stored in that directory. + +The resulting directory structure will look like this: +```toml title="config.toml" +detached-environments = "/opt/pixi/envs" +``` +```shell +/opt/pixi/envs +├── pixi-6837172896226367631 +│ └── envs +└── NAME_OF_PROJECT-HASH_OF_ORIGINAL_PATH + ├── envs # the runnable environments + └── solve-group-envs # If there are solve groups + +``` + +### `mirrors` +Configuration for conda channel-mirrors, more info [below](#mirror-configuration). + +```toml title="config.toml" +[mirrors] +# redirect all requests for conda-forge to the prefix.dev mirror +"https://conda.anaconda.org/conda-forge" = [ + "https://prefix.dev/conda-forge" +] + +# redirect all requests for bioconda to one of the three listed mirrors +# Note: for repodata we try the first mirror first. +"https://conda.anaconda.org/bioconda" = [ + "https://conda.anaconda.org/bioconda", + # OCI registries are also supported + "oci://ghcr.io/channel-mirrors/bioconda", + "https://prefix.dev/bioconda", +] +``` + +### `repodata-config` +Configuration for repodata fetching. +```toml title="config.toml" +[repodata-config] +# disable fetching of jlap, bz2 or zstd repodata files. +# This should only be used for specific old versions of artifactory and other non-compliant +# servers. +disable-jlap = true # don't try to download repodata.jlap +disable-bzip2 = true # don't try to download repodata.json.bz2 +disable-zstd = true # don't try to download repodata.json.zst +``` + +### `pypi-config` +To setup a certain number of defaults for the usage of PyPI registries. You can use the following configuration options: + +- `index-url`: The default index URL to use for PyPI packages. This will be added to a manifest file on a `pixi init`. +- `extra-index-urls`: A list of additional URLs to use for PyPI packages. This will be added to a manifest file on a `pixi init`. +- `keyring-provider`: Allows the use of the [keyring](https://pypi.org/project/keyring/) python package to store and retrieve credentials. + +```toml title="config.toml" +[pypi-config] +# Main index url +index-url = "https://pypi.org/simple" +# list of additional urls +extra-index-urls = ["https://pypi.org/simple2"] +# can be "subprocess" or "disabled" +keyring-provider = "subprocess" +``` + +!!! Note "`index-url` and `extra-index-urls` are *not* globals" + Unlike pip, these settings, with the exception of `keyring-provider` will only modify the `pixi.toml`/`pyproject.toml` file and are not globally interpreted when not present in the manifest. + This is because we want to keep the manifest file as complete and reproducible as possible. + +## Mirror configuration + +You can configure mirrors for conda channels. We expect that mirrors are exact +copies of the original channel. The implementation will look for the mirror key +(a URL) in the `mirrors` section of the configuration file and replace the +original URL with the mirror URL. + +To also include the original URL, you have to repeat it in the list of mirrors. + +The mirrors are prioritized based on the order of the list. We attempt to fetch +the repodata (the most important file) from the first mirror in the list. The +repodata contains all the SHA256 hashes of the individual packages, so it is +important to get this file from a trusted source. + +You can also specify mirrors for an entire "host", e.g. + +```toml title="config.toml" +[mirrors] +"https://conda.anaconda.org" = [ + "https://prefix.dev/" +] +``` + +This will forward all request to channels on anaconda.org to prefix.dev. +Channels that are not currently mirrored on prefix.dev will fail in the above example. + +### OCI Mirrors + +You can also specify mirrors on the OCI registry. There is a public mirror on +the Github container registry (ghcr.io) that is maintained by the conda-forge +team. You can use it like this: + +```toml title="config.toml" +[mirrors] +"https://conda.anaconda.org/conda-forge" = [ + "oci://ghcr.io/channel-mirrors/conda-forge" +] +``` + +The GHCR mirror also contains `bioconda` packages. You can search the [available +packages on Github](https://github.com/orgs/channel-mirrors/packages). diff --git a/docs/reference/configuration.md b/docs/reference/project_configuration.md similarity index 99% rename from docs/reference/configuration.md rename to docs/reference/project_configuration.md index 38c5aeb57..4ed366659 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/project_configuration.md @@ -692,4 +692,4 @@ When an environment comprises several features (including the default feature): ## Global configuration -The global configuration options are documented in the [global configuration](../advanced/global_configuration.md) section. +The global configuration options are documented in the [global configuration](../reference/pixi_configuration.md) section. diff --git a/docs/tutorials/python.md b/docs/tutorials/python.md index 435f661bc..91d4f1eac 100644 --- a/docs/tutorials/python.md +++ b/docs/tutorials/python.md @@ -142,7 +142,7 @@ Which results in the following fields added to the `pyproject.toml`: test = ["pytest"] ``` -After we have added the optional dependencies to the `pyproject.toml`, pixi automatically creates a [`feature`](../reference/configuration.md/#the-feature-and-environments-tables), which can contain a collection of `dependencies`, `tasks`, `channels`, and more. +After we have added the optional dependencies to the `pyproject.toml`, pixi automatically creates a [`feature`](../reference/project_configuration.md/#the-feature-and-environments-tables), which can contain a collection of `dependencies`, `tasks`, `channels`, and more. Sometimes there are packages that aren't available on conda channels but are published on PyPI. We can add these as well, which pixi will solve together with the default dependencies. diff --git a/docs/tutorials/ros2.md b/docs/tutorials/ros2.md index 0932ddc1b..1d31eff29 100644 --- a/docs/tutorials/ros2.md +++ b/docs/tutorials/ros2.md @@ -171,7 +171,7 @@ pixi run hello - You can add [`depends-on`](../features/advanced_tasks.md#depends-on) to the tasks to create a task chain. - You can add [`cwd`](../features/advanced_tasks.md#working-directory) to the tasks to run the task in a different directory from the root of the project. - You can add [`inputs` and `outputs`](../features/advanced_tasks.md#caching) to the tasks to create a task that only runs when the inputs are changed. - - You can use the [`target`](../reference/configuration.md#the-target-table) syntax to run specific tasks on specific machines. + - You can use the [`target`](../reference/project_configuration.md#the-target-table) syntax to run specific tasks on specific machines. ```toml [tasks] diff --git a/mkdocs.yml b/mkdocs.yml index 407eec4da..e17b23f0b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,10 +121,10 @@ nav: - Info Command: advanced/explain_info_command.md - Channel Logic: advanced/channel_priority.md - GitHub Actions: advanced/github_actions.md - - Global Configuration: advanced/global_configuration.md - Pyproject.toml: advanced/pyproject_toml.md - Reference: - - Project Configuration: reference/configuration.md + - Project Configuration: reference/project_configuration.md + - Pixi Configuration: reference/pixi_configuration.md - CLI: reference/cli.md - Misc: - Pixi vision: vision.md @@ -143,7 +143,8 @@ plugins: "design_proposals/multi_environment_proposal.md": "features/multi_environment.md" "advanced/multi_platform_configuration.md": "features/multi_platform_configuration.md" "cli.md": "reference/cli.md" - "configuration.md": "reference/configuration.md" + "configuration.md": "reference/project_configuration.md" + "advanced/global_configuration.md": "reference/pixi_configuration.md" - search - social diff --git a/src/cli/info.rs b/src/cli/info.rs index 93d15565e..4c6be623e 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -37,6 +37,7 @@ pub struct Args { #[derive(Serialize)] pub struct ProjectInfo { + name: String, manifest_path: PathBuf, last_updated: Option, pixi_folder_size: Option, @@ -210,6 +211,7 @@ impl Display for Info { if let Some(pi) = self.project_info.as_ref() { writeln!(f, "\n{}", bold.apply_to("Project\n------------"))?; + writeln!(f, "{:>WIDTH$}: {}", bold.apply_to("Name"), pi.name)?; if let Some(version) = pi.version.clone() { writeln!(f, "{:>WIDTH$}: {}", bold.apply_to("Version"), version)?; } @@ -288,7 +290,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let project = Project::load_or_else_discover(args.manifest_path.as_deref()).ok(); let (pixi_folder_size, cache_size) = if args.extended { - let env_dir = project.as_ref().map(|p| p.root().join(".pixi")); + let env_dir = project.as_ref().map(|p| p.pixi_dir()); let cache_dir = config::get_cache_dir()?; await_in_progress("fetching directory sizes", |_| { spawn_blocking(move || { @@ -304,6 +306,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { }; let project_info = project.clone().map(|p| ProjectInfo { + name: p.name().to_string(), manifest_path: p.manifest_path(), last_updated: last_updated(p.lock_file_path()).ok(), pixi_folder_size, diff --git a/src/cli/install.rs b/src/cli/install.rs index 49c41b675..5a15c5974 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -55,20 +55,29 @@ pub async fn execute(args: Args) -> miette::Result<()> { } // Message what's installed + let detached_envs_message = + if let Ok(Some(path)) = project.config().detached_environments().path() { + format!(" in '{}'", console::style(path.display()).bold()) + } else { + "".to_string() + }; + if installed_envs.len() == 1 { eprintln!( - "{}The {} environment has been installed.", + "{}The {} environment has been installed{}.", console::style(console::Emoji("✔ ", "")).green(), installed_envs[0].fancy_display(), + detached_envs_message ); } else { eprintln!( - "{}The following environments have been installed: \n\t{}", + "{}The following environments have been installed{}: \n\t{}", console::style(console::Emoji("✔ ", "")).green(), installed_envs .iter() .map(|n| n.fancy_display()) .join("\n\t"), + detached_envs_message ); } diff --git a/src/config.rs b/src/config.rs index 0f826b6fb..23aae4685 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,12 @@ use clap::{ArgAction, Parser}; -use miette::{Context, IntoDiagnostic}; +use miette::{miette, Context, IntoDiagnostic}; use rattler_conda_types::{Channel, ChannelConfig, ParseChannelError}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{BTreeSet as Set, HashMap}; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::str::FromStr; use url::Url; @@ -132,8 +133,8 @@ pub enum KeyringProvider { Subprocess, } -#[derive(Clone, Debug, Deserialize, Default, Serialize)] -#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] pub struct PyPIConfig { /// The default index URL for PyPI packages. #[serde(default)] @@ -149,6 +150,35 @@ pub struct PyPIConfig { pub keyring_provider: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum DetachedEnvironments { + Boolean(bool), + Path(PathBuf), +} +impl DetachedEnvironments { + pub fn is_false(&self) -> bool { + matches!(self, DetachedEnvironments::Boolean(false)) + } + + // Get the path to the detached-environments directory. None means the default directory. + pub fn path(&self) -> miette::Result> { + match self { + DetachedEnvironments::Path(p) => Ok(Some(p.clone())), + DetachedEnvironments::Boolean(b) if *b => { + let path = get_cache_dir()?.join(consts::ENVIRONMENTS_DIR); + Ok(Some(path)) + } + _ => Ok(None), + } + } +} +impl Default for DetachedEnvironments { + fn default() -> Self { + DetachedEnvironments::Boolean(false) + } +} + impl PyPIConfig { /// Merge the given PyPIConfig into the current one. pub fn merge(self, other: Self) -> Self { @@ -232,6 +262,13 @@ pub struct Config { #[serde(default)] #[serde(skip_serializing_if = "PyPIConfig::is_default")] pub pypi_config: PyPIConfig, + + /// The option to specify the directory where detached environments are stored. + /// When using 'true', it defaults to the cache directory. + /// When using a path, it uses the specified path. + /// When using 'false', it disables detached environments, meaning it moves it back to the .pixi folder. + #[serde(skip_serializing_if = "Option::is_none")] + pub detached_environments: Option, } impl Default for Config { @@ -246,6 +283,7 @@ impl Default for Config { channel_config: default_channel_config(), repodata_config: None, pypi_config: PyPIConfig::default(), + detached_environments: Some(DetachedEnvironments::default()), } } } @@ -259,6 +297,7 @@ impl From for Config { .pypi_keyring_provider .map(|val| PyPIConfig::default().with_keyring(val)) .unwrap_or_default(), + detached_environments: None, ..Default::default() } } @@ -277,14 +316,23 @@ impl Config { /// /// # Returns /// - /// The parsed config + /// The parsed config, and the unused keys /// /// # Errors /// /// Parsing errors #[inline] - pub fn from_toml(toml: &str) -> miette::Result { - toml_edit::de::from_str(toml).into_diagnostic() + pub fn from_toml(toml: &str) -> miette::Result<(Config, Set)> { + let de = toml_edit::de::Deserializer::from_str(toml).into_diagnostic()?; + + // Deserialize the config and collect unused keys + let mut unused_keys = Set::new(); + let config: Config = serde_ignored::deserialize(de, |path| { + unused_keys.insert(path.to_string()); + }) + .into_diagnostic()?; + + Ok((config, unused_keys)) } /// Load the config from the given path. @@ -301,10 +349,29 @@ impl Config { let s = fs::read_to_string(path) .into_diagnostic() .wrap_err(format!("failed to read config from '{}'", path.display()))?; - let mut config = Config::from_toml(&s)?; + + let (mut config, unused_keys) = Config::from_toml(&s)?; + + if !unused_keys.is_empty() { + tracing::warn!( + "Ignoring '{}' in at {}", + console::style( + unused_keys + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + ) + .yellow(), + path.display() + ); + } + config.loaded_from.push(path.to_path_buf()); tracing::info!("Loaded config from: {}", path.display()); + config.validate()?; + Ok(config) } @@ -338,6 +405,26 @@ impl Config { }) } + /// Validate the config file. + pub fn validate(&self) -> miette::Result<()> { + // Validate the detached environments directory is set correctly + if let Some(detached_environments) = self.detached_environments.clone() { + match detached_environments { + DetachedEnvironments::Boolean(_) => {} + DetachedEnvironments::Path(path) => { + if !path.is_absolute() { + return Err(miette!( + "The `detached-environments` path must be an absolute path: {}", + path.display() + )); + } + } + } + } + + Ok(()) + } + /// Load the global config file from various global paths. /// /// # Returns @@ -416,6 +503,7 @@ impl Config { channel_config: other.channel_config, repodata_config: other.repodata_config.or(self.repodata_config), pypi_config: other.pypi_config.merge(self.pypi_config), + detached_environments: other.detached_environments.or(self.detached_environments), } } @@ -478,6 +566,11 @@ impl Config { &self.mirrors } + /// Retrieve the value for the target_environments_directory field. + pub fn detached_environments(&self) -> DetachedEnvironments { + self.detached_environments.clone().unwrap_or_default() + } + /// Modify this config with the given key and value /// /// # Note @@ -491,6 +584,7 @@ impl Config { "authentication-override-file", "tls-no-verify", "mirrors", + "detached-environments", "repodata-config", "repodata-config.disable-jlap", "repodata-config.disable-bzip2", @@ -529,6 +623,13 @@ impl Config { .into_diagnostic()? .unwrap_or_default(); } + "detached-environments" => { + self.detached_environments = value.map(|v| match v.as_str() { + "true" => DetachedEnvironments::Boolean(true), + "false" => DetachedEnvironments::Boolean(false), + _ => DetachedEnvironments::Path(PathBuf::from(v)), + }); + } key if key.starts_with("repodata-config") => { if key == "repodata-config" { self.repodata_config = value @@ -661,13 +762,32 @@ mod tests { #[test] fn test_config_parse() { - let toml = r#" - default_channels = ["conda-forge"] - tls_no_verify = true - "#; - let config = Config::from_toml(toml).unwrap(); + let toml = format!( + r#"default-channels = ["conda-forge"] +tls-no-verify = true +detached-environments = "{}" +UNUSED = "unused" + "#, + env!("CARGO_MANIFEST_DIR").replace("\\", "\\\\").as_str() + ); + let (config, unused) = Config::from_toml(toml.as_str()).unwrap(); assert_eq!(config.default_channels, vec!["conda-forge"]); assert_eq!(config.tls_no_verify, Some(true)); + assert_eq!( + config.detached_environments().path().unwrap(), + Some(PathBuf::from(env!("CARGO_MANIFEST_DIR"))) + ); + assert!(unused.contains(&"UNUSED".to_string())); + + let toml = r"detached-environments = true"; + let (config, _) = Config::from_toml(toml).unwrap(); + assert_eq!( + config.detached_environments().path().unwrap().unwrap(), + get_cache_dir() + .unwrap() + .join(consts::ENVIRONMENTS_DIR) + .as_path() + ); } #[test] @@ -706,7 +826,7 @@ mod tests { extra-index-urls = ["https://pypi.org/simple2"] keyring-provider = "subprocess" "#; - let config = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml).unwrap(); assert_eq!( config.pypi_config().index_url, Some(Url::parse("https://pypi.org/simple").unwrap()) @@ -725,11 +845,34 @@ mod tests { default_channels: vec!["conda-forge".to_string()], channel_config: ChannelConfig::default_with_root_dir(PathBuf::from("/root/dir")), tls_no_verify: Some(true), + detached_environments: Some(DetachedEnvironments::Path(PathBuf::from("/path/to/envs"))), ..Default::default() }; config = config.merge_config(other); assert_eq!(config.default_channels, vec!["conda-forge"]); assert_eq!(config.tls_no_verify, Some(true)); + assert_eq!( + config.detached_environments().path().unwrap(), + Some(PathBuf::from("/path/to/envs")) + ); + + let other2 = Config { + default_channels: vec!["channel".to_string()], + channel_config: ChannelConfig::default_with_root_dir(PathBuf::from("/root/dir2")), + tls_no_verify: Some(false), + detached_environments: Some(DetachedEnvironments::Path(PathBuf::from( + "/path/to/envs2", + ))), + ..Default::default() + }; + + config = config.merge_config(other2); + assert_eq!(config.default_channels, vec!["channel"]); + assert_eq!(config.tls_no_verify, Some(false)); + assert_eq!( + config.detached_environments().path().unwrap(), + Some(PathBuf::from("/path/to/envs2")) + ); let d = Path::new(&env!("CARGO_MANIFEST_DIR")) .join("tests") @@ -739,6 +882,7 @@ mod tests { let config_2 = Config::from_path(&d.join("config_2.toml")).unwrap(); let config_2 = Config { channel_config: ChannelConfig::default_with_root_dir(PathBuf::from("/root/dir")), + detached_environments: Some(DetachedEnvironments::Boolean(true)), ..config_2 }; @@ -768,7 +912,7 @@ mod tests { disable_bzip2 = true disable_zstd = true "#; - let config = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml).unwrap(); assert_eq!(config.default_channels, vec!["conda-forge"]); assert_eq!(config.tls_no_verify, Some(false)); assert_eq!( @@ -828,6 +972,25 @@ mod tests { Some(PathBuf::from("/path/to/your/override.json")) ); + config + .set("detached-environments", Some("true".to_string())) + .unwrap(); + assert_eq!( + config.detached_environments().path().unwrap().unwrap(), + get_cache_dir() + .unwrap() + .join(consts::ENVIRONMENTS_DIR) + .as_path() + ); + + config + .set("detached-environments", Some("/path/to/envs".to_string())) + .unwrap(); + assert_eq!( + config.detached_environments().path().unwrap(), + Some(PathBuf::from("/path/to/envs")) + ); + config .set("mirrors", Some(r#"{"https://conda.anaconda.org/conda-forge": ["https://prefix.dev/conda-forge"]}"#.to_string())) .unwrap(); diff --git a/src/project/mod.rs b/src/project/mod.rs index f8b291c68..2f4f11c56 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -17,6 +17,10 @@ use reqwest_middleware::ClientWithMiddleware; use std::hash::Hash; use rattler_repodata_gateway::Gateway; + +#[cfg(not(windows))] +use std::os::unix::fs::symlink; + use std::sync::OnceLock; use std::{ collections::{HashMap, HashSet}, @@ -25,6 +29,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use xxhash_rust::xxh3::xxh3_64; use crate::activation::{get_environment_variables, run_activation}; use crate::config::Config; @@ -37,8 +42,11 @@ use manifest::{EnvironmentName, Manifest}; use self::manifest::{pyproject::PyProjectToml, Environments}; pub use dependencies::{CondaDependencies, PyPiDependencies}; pub use environment::Environment; +use once_cell::sync::OnceCell; pub use solve_group::SolveGroup; +static CUSTOM_TARGET_DIR_WARN: OnceCell<()> = OnceCell::new(); + /// The dependency types we support #[derive(Debug, Copy, Clone)] pub enum DependencyType { @@ -206,6 +214,7 @@ impl Project { let env_vars = Project::init_env_vars(&manifest.parsed.environments); + // Load the user configuration from the local project and all default locations let config = Config::load(root); let (client, authenticated_client) = build_reqwest_clients(Some(&config)); @@ -286,13 +295,68 @@ impl Project { self.root.join(consts::PIXI_DIR) } + /// Create the detached-environments path for this project if it is set in the config + fn detached_environments_path(&self) -> Option { + if let Ok(Some(detached_environments_path)) = self.config().detached_environments().path() { + Some(detached_environments_path.join(format!( + "{}-{}", + self.name(), + xxh3_64(self.root.to_string_lossy().as_bytes()) + ))) + } else { + None + } + } + /// Returns the environment directory pub fn environments_dir(&self) -> PathBuf { - self.pixi_dir().join(consts::ENVIRONMENTS_DIR) + let default_envs_dir = self.pixi_dir().join(consts::ENVIRONMENTS_DIR); + + // Early out if detached-environments is not set + if self.config().detached_environments().is_false() { + return default_envs_dir; + } + + // If the detached-environments path is set, use it instead of the default directory. + if let Some(detached_environments_path) = self.detached_environments_path() { + let detached_environments_path = + detached_environments_path.join(consts::ENVIRONMENTS_DIR); + let _ = CUSTOM_TARGET_DIR_WARN.get_or_init(|| { + + #[cfg(not(windows))] + if default_envs_dir.exists() && !default_envs_dir.is_symlink() { + tracing::warn!( + "Environments found in '{}', this will be ignored and the environment will be installed in the 'detached-environments' directory: '{}'. It's advised to remove the {} folder from the default directory to avoid confusion{}.", + default_envs_dir.display(), + detached_environments_path.parent().expect("path should have parent").display(), + format!("{}/{}", consts::PIXI_DIR, consts::ENVIRONMENTS_DIR), + if cfg!(windows) { "" } else { " as a symlink can be made, please re-install after removal." } + ); + } else { + create_symlink(&detached_environments_path, &default_envs_dir); + } + + #[cfg(windows)] + write_warning_file(&default_envs_dir, &detached_environments_path); + }); + + return detached_environments_path; + } + + tracing::debug!( + "Using default root directory: `{}` as environments directory.", + default_envs_dir.display() + ); + + default_envs_dir } - /// Returns the solve group directory + /// Returns the solve group environments directory pub fn solve_group_environments_dir(&self) -> PathBuf { + // If the detached-environments path is set, use it instead of the default directory. + if let Some(detached_environments_path) = self.detached_environments_path() { + return detached_environments_path.join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR); + } self.pixi_dir().join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR) } @@ -470,6 +534,70 @@ pub fn find_project_manifest() -> Option { }) } +/// Create a symlink from the default pixi directory to the custom target directory +#[cfg(not(windows))] +fn create_symlink(pixi_dir_name: &Path, default_pixi_dir: &Path) { + if default_pixi_dir.exists() { + tracing::debug!( + "Symlink already exists at '{}', skipping creating symlink.", + default_pixi_dir.display() + ); + return; + } + symlink(pixi_dir_name, default_pixi_dir) + .map_err(|e| { + tracing::error!( + "Failed to create symlink from '{}' to '{}': {}", + pixi_dir_name.display(), + default_pixi_dir.display(), + e + ) + }) + .ok(); +} + +/// Write a warning file to the default pixi directory to inform the user that symlinks are not supported on this platform (Windows). +#[cfg(windows)] +fn write_warning_file(default_envs_dir: &PathBuf, envs_dir_name: &Path) { + let warning_file = default_envs_dir.join("README.txt"); + if warning_file.exists() { + tracing::debug!( + "Symlink warning file already exists at '{}', skipping writing warning file.", + warning_file.display() + ); + return; + } + let warning_message = format!( + "Environments are installed in a custom detached-environments directory: {}.\n\ + Symlinks are not supported on this platform so environments will not be reachable from the default ('.pixi/envs') directory.", + envs_dir_name.display() + ); + + // Create directory if it doesn't exist + if let Err(e) = std::fs::create_dir_all(default_envs_dir) { + tracing::error!( + "Failed to create directory '{}': {}", + default_envs_dir.display(), + e + ); + return; + } + + // Write warning message to file + match std::fs::write(&warning_file, warning_message.clone()) { + Ok(_) => tracing::info!( + "Symlink warning file written to '{}': {}", + warning_file.display(), + warning_message + ), + Err(e) => tracing::error!( + "Failed to write symlink warning file to '{}': {}", + warning_file.display(), + e + ), + } +} + #[cfg(test)] mod tests { use self::has_features::HasFeatures; diff --git a/src/snapshots/pixi__config__tests__config_merge.snap b/src/snapshots/pixi__config__tests__config_merge.snap index e9061025f..a5957b955 100644 --- a/src/snapshots/pixi__config__tests__config_merge.snap +++ b/src/snapshots/pixi__config__tests__config_merge.snap @@ -54,4 +54,9 @@ Config { extra_index_urls: [], keyring_provider: None, }, + detached_environments: Some( + Boolean( + true, + ), + ), } diff --git a/tests/install_tests.rs b/tests/install_tests.rs index 5154bf688..14fc20138 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -1,13 +1,18 @@ mod common; -use std::path::Path; +use std::fs::{create_dir_all, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; use std::str::FromStr; use crate::common::builders::{string_from_iter, HasDependencyConfig}; use crate::common::package_database::{Package, PackageDatabase}; use common::{LockFileExt, PixiControl}; +use pixi::cli::run::Args; use pixi::cli::{run, LockFileUsageArgs}; +use pixi::config::{Config, DetachedEnvironments}; use pixi::consts::{DEFAULT_ENVIRONMENT_NAME, PIXI_UV_INSTALLER}; +use pixi::FeatureName; use rattler_conda_types::Platform; use serial_test::serial; use tempfile::TempDir; @@ -120,9 +125,23 @@ async fn test_incremental_lock_file() { #[tokio::test] #[serial] #[cfg_attr(not(feature = "slow_integration_tests"), ignore)] -async fn install_locked() { +async fn install_locked_with_config() { let pixi = PixiControl::new().unwrap(); pixi.init().await.unwrap(); + + // Overwrite install location to a target directory + let mut config = Config::default(); + let target_dir = pixi.project_path().join("target"); + config.detached_environments = Some(DetachedEnvironments::Path(target_dir.clone())); + create_dir_all(target_dir.clone()).unwrap(); + + let config_path = pixi.project().unwrap().pixi_dir().join("config.toml"); + create_dir_all(config_path.parent().unwrap()).unwrap(); + + let mut file = File::create(config_path).unwrap(); + file.write_all(toml_edit::ser::to_string(&config).unwrap().as_bytes()) + .unwrap(); + // Add and update lockfile with this version of python let python_version = if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { "python==3.10.0" @@ -163,6 +182,36 @@ async fn install_locked() { Platform::current(), "python==3.9.0" )); + + // Task command depends on the OS + let which_command = if cfg!(target_os = "windows") { + "where python" + } else { + "which python" + }; + + // Verify that the folders are present in the target directory using a task. + pixi.tasks() + .add("which_python".into(), None, FeatureName::Default) + .with_commands([which_command]) + .execute() + .unwrap(); + + let result = pixi + .run(Args { + task: vec!["which_python".to_string()], + manifest_path: None, + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(result.exit_code, 0); + + // Check for correct path in most important path + let line = result.stdout.lines().next().unwrap(); + let target_dir_canonical = target_dir.canonicalize().unwrap(); + let line_path = PathBuf::from(line).canonicalize().unwrap(); + assert!(line_path.starts_with(&target_dir_canonical)); } /// Test `pixi install/run --frozen` functionality