diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e74cebd90..b5c9d1807 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 41804bc10..c1d3ac46f 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -17,7 +17,7 @@ jobs: name: Format, Lint and Test the Python bindings runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - run: | diff --git a/.github/workflows/rust-compile.yml b/.github/workflows/rust-compile.yml index c7684cdf6..7b256be59 100644 --- a/.github/workflows/rust-compile.yml +++ b/.github/workflows/rust-compile.yml @@ -21,7 +21,7 @@ jobs: name: Check intra-doc links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -34,7 +34,7 @@ jobs: name: Format and Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -75,7 +75,7 @@ jobs: # - { name: "Windows-aarch64", target: aarch64-pc-windows-msvc, os: windows-latest, skip-tests: true } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive lfs: ${{ !matrix.skip-tests }} diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index faba46acf..cd56f57f3 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -35,14 +35,14 @@ pub use match_spec::parse::ParseMatchSpecError; pub use match_spec::{MatchSpec, NamelessMatchSpec}; pub use no_arch_type::{NoArchKind, NoArchType}; pub use package_name::{InvalidPackageNameError, PackageName}; -pub use platform::{ParsePlatformError, Platform}; +pub use platform::{Arch, ParseArchError, ParsePlatformError, Platform}; pub use prefix_record::PrefixRecord; pub use repo_data::patches::{PackageRecordPatch, PatchInstructions, RepoDataPatch}; pub use repo_data::{ChannelInfo, ConvertSubdirError, PackageRecord, RepoData}; pub use repo_data_record::RepoDataRecord; pub use run_export::RunExportKind; pub use version::{ - Component, ParseVersionError, ParseVersionErrorKind, Version, VersionWithSource, + Component, ParseVersionError, ParseVersionErrorKind, StrictVersion, Version, VersionWithSource, }; pub use version_spec::VersionSpec; diff --git a/crates/rattler_conda_types/src/platform.rs b/crates/rattler_conda_types/src/platform.rs index effb13087..f3048cb8b 100644 --- a/crates/rattler_conda_types/src/platform.rs +++ b/crates/rattler_conda_types/src/platform.rs @@ -47,6 +47,7 @@ impl Ord for Platform { } /// Known architectures supported by Conda. +#[allow(missing_docs)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum Arch { X86, @@ -356,9 +357,11 @@ impl Arch { } } +/// An error that can occur when parsing an arch from a string. #[derive(Debug, Error, Clone, Eq, PartialEq)] #[error("'{string}' is not a known arch")] pub struct ParseArchError { + /// The arch string that could not be parsed. pub string: String, } diff --git a/crates/rattler_shell/src/activation.rs b/crates/rattler_shell/src/activation.rs index 535ac3c50..7dcc9d063 100644 --- a/crates/rattler_shell/src/activation.rs +++ b/crates/rattler_shell/src/activation.rs @@ -466,6 +466,10 @@ impl Activator { Ok(after_env .into_iter() .filter(|(key, value)| before_env.get(key) != Some(value)) + // this happens on Windows for some reason + // @SET "=C:=C:\Users\robostack\Programs\pixi" + // @SET "=ExitCode=00000000" + .filter(|(key, _)| !key.is_empty()) .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect()) } diff --git a/crates/rattler_shell/src/shell/mod.rs b/crates/rattler_shell/src/shell/mod.rs index 8ff63ee1d..24847d72b 100644 --- a/crates/rattler_shell/src/shell/mod.rs +++ b/crates/rattler_shell/src/shell/mod.rs @@ -350,11 +350,11 @@ pub struct PowerShell { impl Shell for PowerShell { fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> std::fmt::Result { - writeln!(f, "$Env:{} = \"{}\"", env_var, value) + writeln!(f, "${{Env:{}}} = \"{}\"", env_var, value) } fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> std::fmt::Result { - writeln!(f, "$Env:{}=\"\"", env_var) + writeln!(f, "${{Env:{}}}=\"\"", env_var) } fn run_script(&self, f: &mut impl Write, path: &Path) -> std::fmt::Result { @@ -489,15 +489,23 @@ impl ShellEnum { let parent_process_name = parent_process.name().to_lowercase(); tracing::debug!( - "guessing ShellEnum. Parent process name: {}", - &parent_process_name + "Guessing ShellEnum. Parent process name: {} and args: {:?}", + &parent_process_name, + &parent_process.cmd() ); if parent_process_name.contains("bash") { Some(Bash.into()) } else if parent_process_name.contains("zsh") { Some(Zsh.into()) - } else if parent_process_name.contains("xonsh") { + } else if parent_process_name.contains("xonsh") + // xonsh is a python shell, so we need to check if the parent process is python and if it + // contains xonsh in the arguments. + || (parent_process_name.contains("python") + && parent_process + .cmd().iter() + .any(|arg| arg.contains("xonsh"))) + { Some(Xonsh.into()) } else if parent_process_name.contains("fish") { Some(Fish.into()) diff --git a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_append.snap b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_append.snap index 980a9b1d2..e254da860 100644 --- a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_append.snap +++ b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_append.snap @@ -2,6 +2,6 @@ source: crates/rattler_shell/src/activation.rs expression: script --- -$Env:PATH = "$Env:PATH:__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" -$Env:CONDA_PREFIX = "__PREFIX__" +${Env:PATH} = "$Env:PATH:__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" +${Env:CONDA_PREFIX} = "__PREFIX__" diff --git a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_prepend.snap b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_prepend.snap index 07ece15f4..f512621e7 100644 --- a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_prepend.snap +++ b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_prepend.snap @@ -2,6 +2,6 @@ source: crates/rattler_shell/src/activation.rs expression: script --- -$Env:PATH = "__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$Env:PATH" -$Env:CONDA_PREFIX = "__PREFIX__" +${Env:PATH} = "__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$Env:PATH" +${Env:CONDA_PREFIX} = "__PREFIX__" diff --git a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_replace.snap b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_replace.snap index eea35f1ff..2ee7aa139 100644 --- a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_replace.snap +++ b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__test_activation_script_powershell_replace.snap @@ -2,6 +2,6 @@ source: crates/rattler_shell/src/activation.rs expression: script --- -$Env:PATH = "__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" -$Env:CONDA_PREFIX = "__PREFIX__" +${Env:PATH} = "__PREFIX__/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" +${Env:CONDA_PREFIX} = "__PREFIX__" diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index b70ee88e7..4b2afc49a 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -44,6 +44,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "blake2" version = "0.10.6" @@ -91,9 +97,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "f56b4c72906975ca04becb8a30e102dfecddd0c06181e3e95ddc444be28881f8" dependencies = [ "android-tzdata", "iana-time-zone", @@ -102,7 +108,7 @@ dependencies = [ "serde", "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] @@ -191,12 +197,51 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fnv" version = "1.0.7" @@ -387,6 +432,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + [[package]] name = "lock_api" version = "0.4.10" @@ -508,7 +559,9 @@ version = "0.1.0" dependencies = [ "pyo3", "rattler_conda_types", + "rattler_shell", "thiserror", + "url", ] [[package]] @@ -630,20 +683,35 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "rattler_shell" +version = "0.8.0" +dependencies = [ + "enum_dispatch", + "indexmap 2.0.0", + "itertools", + "rattler_conda_types", + "serde_json", + "shlex", + "tempfile", + "thiserror", + "tracing", +] + [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", @@ -653,9 +721,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", @@ -664,9 +732,22 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustix" +version = "0.38.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] [[package]] name = "rustversion" @@ -688,9 +769,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -706,9 +787,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", @@ -721,6 +802,7 @@ version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ + "indexmap 2.0.0", "itoa", "ryu", "serde", @@ -790,6 +872,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "smallvec" version = "1.11.0" @@ -861,6 +949,19 @@ version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.47" @@ -1008,9 +1109,9 @@ checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -1115,6 +1216,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.48.2" diff --git a/py-rattler/Cargo.toml b/py-rattler/Cargo.toml index e8e27647d..8ddab8267 100644 --- a/py-rattler/Cargo.toml +++ b/py-rattler/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] rattler_conda_types = { path = "../crates/rattler_conda_types", default-features = false } +rattler_shell = { path = "../crates/rattler_shell", default-features = false } pyo3 = { version = "0.19", features = [ "abi3-py38", @@ -18,6 +19,7 @@ pyo3 = { version = "0.19", features = [ ] } thiserror = "1.0.44" +url = "2.4.1" # Prevent package from thinking it's in the workspace [workspace] diff --git a/py-rattler/pyproject.toml b/py-rattler/pyproject.toml index 279641224..ff810e920 100644 --- a/py-rattler/pyproject.toml +++ b/py-rattler/pyproject.toml @@ -21,3 +21,6 @@ license = "BSD-3-Clause" [tool.maturin] features = ["pyo3/extension-module"] + +[tool.ruff] +line-length = 120 diff --git a/py-rattler/rattler/__init__.py b/py-rattler/rattler/__init__.py index 4d084172b..a9e6310e8 100644 --- a/py-rattler/rattler/__init__.py +++ b/py-rattler/rattler/__init__.py @@ -1,5 +1,13 @@ from rattler.version import Version from rattler.match_spec import MatchSpec, NamelessMatchSpec from rattler.repo_data import PackageRecord +from rattler.channel import Channel, ChannelConfig -__all__ = ["Version", "MatchSpec", "NamelessMatchSpec", "PackageRecord"] +__all__ = [ + "Version", + "MatchSpec", + "NamelessMatchSpec", + "PackageRecord", + "Channel", + "ChannelConfig", +] diff --git a/py-rattler/rattler/channel/__init__.py b/py-rattler/rattler/channel/__init__.py new file mode 100644 index 000000000..f4c10ff1c --- /dev/null +++ b/py-rattler/rattler/channel/__init__.py @@ -0,0 +1,4 @@ +from rattler.channel.channel import Channel +from rattler.channel.channel_config import ChannelConfig + +__all__ = ["Channel", "ChannelConfig"] diff --git a/py-rattler/rattler/channel/channel.py b/py-rattler/rattler/channel/channel.py new file mode 100644 index 000000000..f3fd51fc0 --- /dev/null +++ b/py-rattler/rattler/channel/channel.py @@ -0,0 +1,45 @@ +from __future__ import annotations +from typing import Optional + +from rattler.rattler import PyChannel +from rattler.channel.channel_config import ChannelConfig + + +class Channel: + def __init__(self, name: str, channel_configuration: ChannelConfig): + """ + Create a new channel. + + >>> channel = Channel("conda-forge", ChannelConfig()) + >>> channel + Channel(name="conda-forge", base_url="https://conda.anaconda.org/conda-forge/") + """ + self._channel = PyChannel(name, channel_configuration._channel_configuration) + + @property + def name(self) -> Optional[str]: + """ + Return the name of this channel. + + >>> channel = Channel("conda-forge", ChannelConfig()) + >>> channel.name + 'conda-forge' + """ + return self._channel.name + + @property + def base_url(self) -> str: + """ + Return the base URL of this channel. + + >>> channel = Channel("conda-forge", ChannelConfig()) + >>> channel.base_url + 'https://conda.anaconda.org/conda-forge/' + """ + return self._channel.base_url + + def __repr__(self) -> str: + """ + Return a string representation of this channel. + """ + return f'Channel(name="{self.name}", base_url="{self.base_url}")' diff --git a/py-rattler/rattler/channel/channel_config.py b/py-rattler/rattler/channel/channel_config.py new file mode 100644 index 000000000..09480f6c2 --- /dev/null +++ b/py-rattler/rattler/channel/channel_config.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from rattler.rattler import PyChannelConfig + + +class ChannelConfig: + def __init__(self, channel_alias="https://conda.anaconda.org/"): + """ + Create a new channel configuration. + + >>> channel_config = ChannelConfig() + >>> channel_config + ChannelConfig(channel_alias="https://conda.anaconda.org/") + + >>> channel_config = ChannelConfig("https://repo.prefix.dev/") + >>> channel_config + ChannelConfig(channel_alias="https://repo.prefix.dev/") + """ + self._channel_configuration = PyChannelConfig(channel_alias) + + def __repr__(self) -> str: + """ + Return a string representation of this channel configuration. + """ + alias = self._channel_configuration.channel_alias + return f'ChannelConfig(channel_alias="{alias}")' diff --git a/py-rattler/rattler/platform/__init__.py b/py-rattler/rattler/platform/__init__.py new file mode 100644 index 000000000..62603805a --- /dev/null +++ b/py-rattler/rattler/platform/__init__.py @@ -0,0 +1,3 @@ +from rattler.platform.platform import Platform + +__all__ = ["Platform"] diff --git a/py-rattler/rattler/platform/platform.py b/py-rattler/rattler/platform/platform.py new file mode 100644 index 000000000..e269e8912 --- /dev/null +++ b/py-rattler/rattler/platform/platform.py @@ -0,0 +1,168 @@ +from __future__ import annotations +from typing import Literal + +from rattler.rattler import PyPlatform, PyArch + + +ArchLiteral = Literal[ + "x86", + "x86_64", + "aarch64", + "armv6l", + "armv7l", + "ppc64le", + "ppc64", + "s390x", + "riscv32", + "riscv64", +] + + +class Arch: + def __init__(self, value: ArchLiteral): + self._inner = PyArch(value) + + @classmethod + def _from_py_arch(cls, py_arch: PyArch) -> Arch: + """Construct Rattler version from FFI PyArch object.""" + arch = cls.__new__(cls) + arch._inner = py_arch + return arch + + def __str__(self): + """ + Returns a string representation of the architecture. + + >>> str(Arch("x86_64")) + 'x86_64' + """ + return self._inner.as_str() + + def __repr__(self) -> str: + """ + Returns a representation of the architecture. + + >>> Arch("aarch64") + Arch(aarch64) + """ + return f"Arch({self._inner.as_str()})" + + +PlatformLiteral = Literal[ + "noarch", + "linux-32", + "linux-64", + "linux-aarch64", + "linux-armv6l", + "linux-armv7l", + "linux-ppc64le", + "linux-ppc64", + "linux-s390x", + "linux-riscv32", + "linux-riscv64", + "osx-64", + "osx-arm64", + "win-32", + "win-64", + "win-arm64", + "emscripten-32", +] + + +class Platform: + def __init__(self, value: PlatformLiteral): + self._inner = PyPlatform(value) + + @classmethod + def _from_py_platform(cls, py_platform: PyPlatform) -> Platform: + """Construct Rattler version from FFI PyArch object.""" + platform = cls.__new__(cls) + platform._inner = py_platform + return platform + + def __str__(self): + """ + Returns a string representation of the platform. + + >>> str(Platform("linux-64")) + 'linux-64' + """ + return self._inner.name + + def __repr__(self) -> str: + """ + Returnrs a representation of the platform. + + >>> Platform("linux-64") + Platform(linux-64) + """ + return f"Platform({self._inner.name})" + + def current() -> Platform: + """ + Returns the current platform. + + # >>> Platform.current() + # Platform(linux-64) + """ + return Platform._from_py_platform(PyPlatform.current()) + + @property + def is_linux(self): + """ + Return True if the platform is linux. + + >>> Platform("linux-64").is_linux + True + >>> Platform("osx-64").is_linux + False + """ + return self._inner.is_linux + + @property + def is_osx(self): + """ + Return True if the platform is osx. + + >>> Platform("osx-64").is_osx + True + >>> Platform("linux-64").is_osx + False + """ + return self._inner.is_osx + + @property + def is_windows(self): + """ + Return True if the platform is win. + + >>> Platform("win-64").is_windows + True + >>> Platform("linux-64").is_windows + False + """ + return self._inner.is_windows + + @property + def is_unix(self): + """ + Return True if the platform is unix. + + >>> Platform("linux-64").is_unix + True + >>> Platform("win-64").is_unix + False + """ + return self._inner.is_unix + + @property + def arch(self): + """ + Return the architecture of the platform. + + >>> Platform("linux-64").arch + Arch(x86_64) + >>> Platform("linux-aarch64").arch + Arch(aarch64) + """ + return Arch._from_py_arch(self._inner.arch()) diff --git a/py-rattler/rattler/shell/__init__.py b/py-rattler/rattler/shell/__init__.py new file mode 100644 index 000000000..e95f905df --- /dev/null +++ b/py-rattler/rattler/shell/__init__.py @@ -0,0 +1,3 @@ +from rattler.shell.shell import ActivationVariables + +__all__ = ["ActivationVariables"] diff --git a/py-rattler/rattler/shell/shell.py b/py-rattler/rattler/shell/shell.py new file mode 100644 index 000000000..c6b04a202 --- /dev/null +++ b/py-rattler/rattler/shell/shell.py @@ -0,0 +1,130 @@ +from __future__ import annotations +from enum import Enum + +from typing import Iterable, Optional +from pathlib import Path +import os +import sys +from rattler.platform.platform import Platform + +from rattler.rattler import ( + PyActivationVariables, + PyActivator, + PyShellEnum, + PyActivationResult, +) + + +class PathModificationBehavior(Enum): + """ + The behavior to use when modifying the PATH environment variable. + Prepend will add the new path to the beginning of the PATH variable. + Append will add the new path to the end of the PATH variable. + Replace will replace the entire PATH variable with the new path. + """ + + Prepend = "prepend" + Append = "append" + Replace = "replace" + + +class ActivationVariables: + """An object that holds the state of the current environment.""" + + def __init__( + self, + current_prefix: Optional[os.PathLike] = None, + current_path: Optional[Iterable[str] | Iterable[os.PathLike]] = sys.path, + path_modification_behavior: PathModificationBehavior = PathModificationBehavior.Prepend, + ): + """ + Construct a new ActivationVariables object. + + current_prefix: The current activated conda prefix (usually + `sys.env["CONDA_PREFIX"]`). This prefix is going to be deactivated. + current_path: The current PATH environment variable (usually `sys.path`). + path_modification_behavior: The behavior to use when modifying the PATH + environment variable. One of "Prepend", "Append", or "Replace". + Defaults to "Prepend". + """ + self._activation_variables = PyActivationVariables( + current_prefix, current_path, path_modification_behavior.value + ) + + def __str__(self) -> str: + return self._activation_variables.as_str() + + +class ActivationResult: + """An object that holds the result of activating a conda environment.""" + + @classmethod + def _from_py_activation_result( + cls, py_activation_result: PyActivationResult + ) -> ActivationResult: + """Construct Rattler version from FFI PyActivationResult object.""" + activation_result = cls.__new__(cls) + activation_result._py_activation_result = py_activation_result + return activation_result + + @property + def path(self) -> Path: + """The new PATH environment variable.""" + return self._py_activation_result.path + + @property + def script(self) -> str: + """The script to run to activate the environment.""" + return self._py_activation_result.script + + +class Shell: + """An enum of supported shells.""" + + bash = PyShellEnum.Bash + zsh = PyShellEnum.Zsh + fish = PyShellEnum.Fish + xonsh = PyShellEnum.Xonsh + powershell = PyShellEnum.PowerShell + cmd_exe = PyShellEnum.CmdExe + + +def activate( + prefix: Path, + activation_variables: ActivationVariables, + shell: Shell, + platform: Optional[Platform | str] = None, +) -> ActivationResult: + """ + Return an ActivationResult object that contains the new PATH environment variable + and the script to run to activate the environment. + + Arguments: + prefix: The path to the conda prefix to activate. + activation_variables: The current activation variables. + shell: The shell to generate the activation script for. + platform: The platform to generate the activation script for. + If None, the current platform is used. + + Returns: + An ActivationResult object containing the new PATH environment variable + and the script to run to activate the environment. + + Examples: + >>> from rattler.shell.shell import Shell, activate, ActivationVariables + >>> from rattler.platform import Platform + >>> from pathlib import Path + >>> import sys + >>> p = Path("/path/to/conda/prefix") + >>> actvars = ActivationVariables(None, sys.path) + >>> a = activate(p, actvars, Shell.xonsh) + >>> print(a) + + """ + platform = platform or Platform.current() + shell = shell or Shell.bash + return ActivationResult._from_py_activation_result( + PyActivator.activate( + prefix, activation_variables._activation_variables, platform._inner, shell + ) + ) diff --git a/py-rattler/src/channel/mod.rs b/py-rattler/src/channel/mod.rs new file mode 100644 index 000000000..fa4ac648c --- /dev/null +++ b/py-rattler/src/channel/mod.rs @@ -0,0 +1,71 @@ +use pyo3::{pyclass, pymethods}; +use rattler_conda_types::{Channel, ChannelConfig}; +use url::Url; + +use crate::error::PyRattlerError; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyChannelConfig { + pub(crate) inner: ChannelConfig, +} + +#[pymethods] +impl PyChannelConfig { + #[new] + pub fn __init__(channel_alias: &str) -> pyo3::PyResult { + Ok(Self { + inner: ChannelConfig { + channel_alias: Url::parse(channel_alias).map_err(PyRattlerError::from)?, + }, + }) + } + + /// Return the channel alias that is configured + #[getter] + fn channel_alias(&self) -> String { + self.inner.channel_alias.to_string() + } +} + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyChannel { + pub(crate) inner: Channel, +} + +impl From for PyChannel { + fn from(value: Channel) -> Self { + Self { inner: value } + } +} + +impl From for Channel { + fn from(val: PyChannel) -> Self { + val.inner + } +} + +#[pymethods] +impl PyChannel { + #[new] + pub fn __init__(version: &str, config: &PyChannelConfig) -> pyo3::PyResult { + Ok(Channel::from_str(version, &config.inner) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Return the name of the channel + #[getter] + fn name(&self) -> Option { + self.inner.name.clone() + } + + /// Return the base url of the channel + #[getter] + fn base_url(&self) -> String { + self.inner.base_url.to_string() + } +} diff --git a/py-rattler/src/error.rs b/py-rattler/src/error.rs index 4b7feefc5..04423908f 100644 --- a/py-rattler/src/error.rs +++ b/py-rattler/src/error.rs @@ -1,6 +1,10 @@ use pyo3::exceptions::PyException; use pyo3::{create_exception, PyErr}; -use rattler_conda_types::{InvalidPackageNameError, ParseMatchSpecError, ParseVersionError}; +use rattler_conda_types::{ + InvalidPackageNameError, ParseArchError, ParseChannelError, ParseMatchSpecError, + ParsePlatformError, ParseVersionError, +}; +use rattler_shell::activation::ActivationError; use thiserror::Error; #[derive(Error, Debug)] @@ -12,6 +16,16 @@ pub enum PyRattlerError { InvalidMatchSpec(#[from] ParseMatchSpecError), #[error(transparent)] InvalidPackageName(#[from] InvalidPackageNameError), + #[error(transparent)] + InvalidUrl(#[from] url::ParseError), + #[error(transparent)] + InvalidChannel(#[from] ParseChannelError), + #[error(transparent)] + ActivationError(#[from] ActivationError), + #[error(transparent)] + ParsePlatformError(#[from] ParsePlatformError), + #[error(transparent)] + ParseArchError(#[from] ParseArchError), } impl From for PyErr { @@ -26,6 +40,15 @@ impl From for PyErr { PyRattlerError::InvalidPackageName(err) => { InvalidPackageNameException::new_err(err.to_string()) } + PyRattlerError::InvalidUrl(err) => InvalidUrlException::new_err(err.to_string()), + PyRattlerError::InvalidChannel(err) => { + InvalidChannelException::new_err(err.to_string()) + } + PyRattlerError::ActivationError(err) => ActivationException::new_err(err.to_string()), + PyRattlerError::ParsePlatformError(err) => { + ParsePlatformException::new_err(err.to_string()) + } + PyRattlerError::ParseArchError(err) => ParseArchException::new_err(err.to_string()), } } } @@ -33,3 +56,8 @@ impl From for PyErr { create_exception!(exceptions, InvalidVersionException, PyException); create_exception!(exceptions, InvalidMatchSpecException, PyException); create_exception!(exceptions, InvalidPackageNameException, PyException); +create_exception!(exceptions, InvalidUrlException, PyException); +create_exception!(exceptions, InvalidChannelException, PyException); +create_exception!(exceptions, ActivationException, PyException); +create_exception!(exceptions, ParsePlatformException, PyException); +create_exception!(exceptions, ParseArchException, PyException); diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index c73b6fd19..71c399c95 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,11 +1,17 @@ +mod channel; mod error; mod match_spec; mod nameless_match_spec; +mod platform; mod repo_data; +mod shell; mod version; +use channel::{PyChannel, PyChannelConfig}; use error::{ - InvalidMatchSpecException, InvalidPackageNameException, InvalidVersionException, PyRattlerError, + ActivationException, InvalidChannelException, InvalidMatchSpecException, + InvalidPackageNameException, InvalidUrlException, InvalidVersionException, ParseArchException, + ParsePlatformException, PyRattlerError, }; use match_spec::PyMatchSpec; use nameless_match_spec::PyNamelessMatchSpec; @@ -14,6 +20,9 @@ use version::PyVersion; use pyo3::prelude::*; +use platform::{PyArch, PyPlatform}; +use shell::{PyActivationResult, PyActivationVariables, PyActivator, PyShellEnum}; + #[pymodule] fn rattler(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); @@ -23,6 +32,17 @@ fn rattler(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + + // Shell activation things + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + // Exceptions m.add( "InvalidVersionError", @@ -39,6 +59,22 @@ fn rattler(py: Python, m: &PyModule) -> PyResult<()> { py.get_type::(), ) .unwrap(); + m.add("InvalidUrlError", py.get_type::()) + .unwrap(); + m.add( + "InvalidChannelError", + py.get_type::(), + ) + .unwrap(); + m.add("ActivationError", py.get_type::()) + .unwrap(); + m.add( + "ParsePlatformError", + py.get_type::(), + ) + .unwrap(); + m.add("ParseArchError", py.get_type::()) + .unwrap(); Ok(()) } diff --git a/py-rattler/src/platform.rs b/py-rattler/src/platform.rs new file mode 100644 index 000000000..0de199339 --- /dev/null +++ b/py-rattler/src/platform.rs @@ -0,0 +1,123 @@ +use std::str::FromStr; + +use pyo3::{pyclass, pymethods}; +use rattler_conda_types::{Arch, Platform}; + +use crate::error::PyRattlerError; + +/////////////////////////// +/// Arch /// +/////////////////////////// + +#[pyclass] +#[derive(Clone)] +pub struct PyArch { + pub inner: Arch, +} + +impl From for PyArch { + fn from(value: Arch) -> Self { + PyArch { inner: value } + } +} + +impl FromStr for PyArch { + type Err = PyRattlerError; + + fn from_str(s: &str) -> Result { + let arch = Arch::from_str(s).map_err(PyRattlerError::from)?; + Ok(arch.into()) + } +} + +#[pymethods] +impl PyArch { + #[new] + pub fn __init__(arch: &str) -> Result { + let arch = Arch::from_str(arch).map_err(PyRattlerError::from)?; + Ok(arch.into()) + } + + #[staticmethod] + pub fn current() -> Self { + Arch::current().into() + } + + pub fn as_str(&self) -> &str { + self.inner.as_str() + } +} + +/////////////////////////// +/// Platform /// +/////////////////////////// + +#[pyclass] +#[derive(Clone)] +pub struct PyPlatform { + pub inner: Platform, +} + +impl From for PyPlatform { + fn from(value: Platform) -> Self { + PyPlatform { inner: value } + } +} + +impl From for Platform { + fn from(value: PyPlatform) -> Self { + value.inner + } +} + +impl FromStr for PyPlatform { + type Err = PyRattlerError; + + fn from_str(s: &str) -> Result { + let platform = Platform::from_str(s).map_err(PyRattlerError::from)?; + Ok(platform.into()) + } +} + +#[pymethods] +impl PyPlatform { + #[new] + pub fn __init__(platform: &str) -> Result { + let platform = Platform::from_str(platform).map_err(PyRattlerError::from)?; + Ok(platform.into()) + } + + #[staticmethod] + pub fn current() -> Self { + Platform::current().into() + } + + #[getter] + pub fn name(&self) -> String { + self.inner.to_string() + } + + #[getter] + pub fn is_windows(&self) -> bool { + self.inner.is_windows() + } + + #[getter] + pub fn is_linux(&self) -> bool { + self.inner.is_linux() + } + + #[getter] + pub fn is_osx(&self) -> bool { + self.inner.is_osx() + } + + #[getter] + pub fn is_unix(&self) -> bool { + self.inner.is_unix() + } + + pub fn arch(&self) -> Option { + self.inner.arch().map(Into::into) + } +} diff --git a/py-rattler/src/shell.rs b/py-rattler/src/shell.rs new file mode 100644 index 000000000..84d1f7011 --- /dev/null +++ b/py-rattler/src/shell.rs @@ -0,0 +1,152 @@ +use crate::error::PyRattlerError; +use crate::platform::PyPlatform; +use pyo3::{exceptions::PyValueError, pyclass, pymethods, FromPyObject, PyAny, PyResult}; +use rattler_shell::{ + activation::{ActivationResult, ActivationVariables, Activator, PathModificationBehaviour}, + shell::{Bash, CmdExe, Fish, PowerShell, Xonsh, Zsh}, +}; +use std::path::{Path, PathBuf}; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyActivationVariables { + inner: ActivationVariables, +} + +impl From for PyActivationVariables { + fn from(value: ActivationVariables) -> Self { + PyActivationVariables { inner: value } + } +} + +#[repr(transparent)] +pub struct Wrap(pub T); + +impl FromPyObject<'_> for Wrap { + fn extract(ob: &PyAny) -> PyResult { + let parsed = match ob.extract::<&str>()? { + "prepend" => PathModificationBehaviour::Prepend, + "append" => PathModificationBehaviour::Append, + "replace" => PathModificationBehaviour::Replace, + v => { + return Err(PyValueError::new_err(format!( + "keep must be one of {{'prepend', 'append', 'replace'}}, got {v}", + ))) + } + }; + Ok(Wrap(parsed)) + } +} + +#[pymethods] +impl PyActivationVariables { + #[new] + #[pyo3(signature = (conda_prefix, path, path_modification_behaviour))] + pub fn __init__( + conda_prefix: Option, + path: Option>, + path_modification_behaviour: Wrap, + ) -> Self { + let activation_vars = ActivationVariables { + conda_prefix, + path, + path_modification_behaviour: path_modification_behaviour.0, + }; + activation_vars.into() + } + + #[getter] + pub fn conda_prefix(&self) -> Option<&Path> { + self.inner.conda_prefix.as_deref() + } + + #[getter] + pub fn path(&self) -> Option> { + self.inner + .path + .as_ref() + .map(|p| p.iter().map(|p| p.as_path()).collect()) + } +} + +#[pyclass] +pub struct PyActivationResult { + pub inner: ActivationResult, +} + +impl From for PyActivationResult { + fn from(value: ActivationResult) -> Self { + PyActivationResult { inner: value } + } +} + +#[pymethods] +impl PyActivationResult { + #[getter] + pub fn path(&self) -> Vec { + self.inner.path.clone() + } + + #[getter] + pub fn script(&self) -> String { + self.inner.script.clone() + } +} + +#[pyclass] +#[derive(Clone)] +pub enum PyShellEnum { + Bash, + Zsh, + Xonsh, + CmdExe, + PowerShell, + Fish, +} + +#[pyclass] +pub struct PyActivator; + +#[pymethods] +impl PyActivator { + #[staticmethod] + pub fn activate( + prefix: PathBuf, + activation_vars: PyActivationVariables, + platform: PyPlatform, + shell: PyShellEnum, + ) -> Result { + let activation_vars = activation_vars.inner; + let activation_result = match shell { + PyShellEnum::Bash => { + Activator::::from_path(prefix.as_path(), Bash, platform.into())? + .activation(activation_vars)? + } + PyShellEnum::Zsh => { + Activator::::from_path(prefix.as_path(), Zsh, platform.into())? + .activation(activation_vars)? + } + PyShellEnum::Xonsh => { + Activator::::from_path(prefix.as_path(), Xonsh, platform.into())? + .activation(activation_vars)? + } + PyShellEnum::CmdExe => { + Activator::::from_path(prefix.as_path(), CmdExe, platform.into())? + .activation(activation_vars)? + } + PyShellEnum::PowerShell => Activator::::from_path( + prefix.as_path(), + PowerShell::default(), + platform.into(), + )? + .activation(activation_vars)?, + PyShellEnum::Fish => { + Activator::::from_path(prefix.as_path(), Fish, platform.into())? + .activation(activation_vars)? + } + }; + + Ok(activation_result.into()) + } +}