Skip to content
Merged
4 changes: 2 additions & 2 deletions docs/reference/cli/pixi/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pixi import [OPTIONS] <FILE>
## Options
- <a id="arg---format" href="#arg---format">`--format <FORMAT>`</a>
: Which format to interpret the file as
<br>**options**: `conda-env`
<br>**options**: `conda-env`, `pypi-txt`
- <a id="arg---platform" href="#arg---platform">`--platform (-p) <PLATFORM>`</a>
: The platforms for the imported environment
<br>May be provided more than once.
Expand Down Expand Up @@ -69,7 +69,7 @@ pixi import [OPTIONS] <FILE>
## Description
Imports a file into an environment in an existing workspace.

If `--format` isn't provided, `import` will try to guess the format based on the file extension.
If `--format` isn't provided, `import` will try each format in turn


--8<-- "docs/reference/cli/pixi/import_extender:example"
160 changes: 117 additions & 43 deletions src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ use clap::{Parser, ValueEnum};
use pixi_config::{Config, ConfigCli};
use pixi_manifest::{EnvironmentName, FeatureName, HasFeaturesIter, PrioritizedChannel};
use pixi_utils::conda_environment_file::CondaEnvFile;
use pixi_uv_conversions::convert_uv_requirements_to_pep508;
use rattler_conda_types::Platform;

use tracing::warn;
use uv_client::BaseClientBuilder;
use uv_requirements_txt::RequirementsTxt;

use miette::{Diagnostic, IntoDiagnostic, Result};
use thiserror::Error;

Expand All @@ -19,14 +24,15 @@ use crate::{

#[derive(Parser, Debug, Clone, PartialEq, ValueEnum)]
pub enum ImportFileFormat {
// TODO: implement conda-lock, conda-txt, pypi-txt
// TODO: implement conda-lock, conda-txt
CondaEnv,
PypiTxt,
}

/// Imports a file into an environment in an existing workspace.
///
/// If `--format` isn't provided, `import` will try to guess the format based on the file extension.
#[derive(Parser, Debug, Default)]
/// If `--format` isn't provided, `import` will try each format in turn
#[derive(Parser, Debug, Default, Clone)]
#[clap(arg_required_else_help = true, verbatim_doc_comment)]
pub struct Args {
#[clap(flatten)]
Expand Down Expand Up @@ -64,32 +70,29 @@ pub struct Args {

pub async fn execute(args: Args) -> miette::Result<()> {
if let Some(format) = &args.format {
if *format != ImportFileFormat::CondaEnv {
miette::bail!(
"Only the conda environment.yml format is supported currently. Please pass `conda-env` to `format`."
);
}
import_conda_env(args).await
import(args.clone(), format).await
} else if let Ok(result) = import(args.clone(), &ImportFileFormat::CondaEnv).await {
return Ok(result);
} else if let Ok(result) = import(args, &ImportFileFormat::PypiTxt).await {
return Ok(result);
} else {
import_conda_env(args).await // .or_else(...)
miette::bail!(
"Tried all formats for input file, but none were successful. Pass a `format` argument to see the specific error for that format."
)
}
}

#[derive(Debug, Error, Diagnostic)]
#[error("Missing name: provide --feature or --environment, or set `name:` in input file.")]
#[error(
"Missing name: provide --feature or --environment, or set `name:` in input file for the conda-env format."
)]
struct MissingEnvironmentName;

fn get_feature_and_environment(
feature_arg: &Option<String>,
environment_arg: &Option<String>,
file: &CondaEnvFile,
) -> Result<(String, String), miette::Report> {
let fallback = || {
file.name()
.map(|s| s.to_string())
.ok_or(MissingEnvironmentName)
};

fallback: impl Fn() -> Result<String, MissingEnvironmentName>,
) -> Result<(FeatureName, EnvironmentName), miette::Report> {
let feature_string = match (feature_arg, environment_arg) {
(Some(f), _) => f.clone(),
(_, Some(e)) => e.clone(),
Expand All @@ -102,11 +105,40 @@ fn get_feature_and_environment(
_ => fallback()?,
};

Ok((feature_string, environment_string))
Ok((
FeatureName::from(feature_string),
EnvironmentName::from_str(&environment_string)?,
))
}

fn convert_uv_requirements_txt_to_pep508(
reqs_txt: uv_requirements_txt::RequirementsTxt,
) -> Result<Vec<pep508_rs::Requirement>, miette::Error> {
let uv_requirements: Vec<uv_pep508::Requirement<uv_pypi_types::VerbatimParsedUrl>> = reqs_txt
.requirements
.into_iter()
.map(|r| match r.requirement {
uv_requirements_txt::RequirementsTxtRequirement::Named(req) => Ok(req),
uv_requirements_txt::RequirementsTxtRequirement::Unnamed(_) => Err(miette::miette!(
"Error parsing input file: unnamed requirements are currently unsupported."
)),
})
.collect::<Result<_, _>>()?;
if !reqs_txt.constraints.is_empty() {
warn!(
"Constraints detected in input file, but these are currently unsupported. Continuing without applying constraints..."
)
}

let requirements =
convert_uv_requirements_to_pep508(uv_requirements.iter()).into_diagnostic()?;

Ok(requirements)
}

async fn import_conda_env(args: Args) -> miette::Result<()> {
let (file, platforms, workspace_config) = (args.file, args.platforms, args.workspace_config);
async fn import(args: Args, format: &ImportFileFormat) -> miette::Result<()> {
let (input_file, platforms, workspace_config) =
(args.file, args.platforms, args.workspace_config);
let config = Config::from(args.config);

let workspace = WorkspaceLocator::for_cli()
Expand All @@ -120,10 +152,37 @@ async fn import_conda_env(args: Args) -> miette::Result<()> {

// TODO: add dry_run logic to import

let file = CondaEnvFile::from_path(&file)?;
let (feature_string, environment_string) =
get_feature_and_environment(&args.feature, &args.environment, &file)?;
let feature_name = FeatureName::from(feature_string.clone());
enum ProcessedInput {
CondaEnv(CondaEnvFile),
PypiTxt,
}

let (processed_input, feature_name, environment_name) = match format {
ImportFileFormat::CondaEnv => {
let env_file = CondaEnvFile::from_path(&input_file)?;
let fallback = || {
env_file
.name()
.map(|s| s.to_string())
.ok_or(MissingEnvironmentName)
};
let (feature_name, environment_name) =
get_feature_and_environment(&args.feature, &args.environment, fallback)?;

(
ProcessedInput::CondaEnv(env_file),
feature_name,
environment_name,
)
}
ImportFileFormat::PypiTxt => {
let (feature_name, environment_name) =
get_feature_and_environment(&args.feature, &args.environment, || {
Err(MissingEnvironmentName)
})?;
(ProcessedInput::PypiTxt, feature_name, environment_name)
}
};

// Add the platforms if they are not already present
if !platforms.is_empty() {
Expand All @@ -132,29 +191,44 @@ async fn import_conda_env(args: Args) -> miette::Result<()> {
.add_platforms(platforms.iter(), &feature_name)?;
}

// TODO: handle `variables` section
// let env_vars = file.variables();
let (conda_deps, pypi_deps) = match processed_input {
ProcessedInput::CondaEnv(env_file) => {
// TODO: handle `variables` section
// let env_vars = file.variables();

// TODO: Improve this:
// - Use .condarc as channel config
let (conda_deps, pypi_deps, channels) = env_file.to_manifest(&config.clone())?;
workspace.manifest().add_channels(
channels.iter().map(|c| PrioritizedChannel::from(c.clone())),
&feature_name,
false,
)?;

// TODO: Improve this:
// - Use .condarc as channel config
let (conda_deps, pypi_deps, channels) = file.to_manifest(&config.clone())?;
workspace.manifest().add_channels(
channels.iter().map(|c| PrioritizedChannel::from(c.clone())),
&feature_name,
false,
)?;
(conda_deps, pypi_deps)
}
ProcessedInput::PypiTxt => {
let reqs_txt = RequirementsTxt::parse(
&input_file,
workspace.workspace().root(),
&BaseClientBuilder::new(),
)
.await
.into_diagnostic()?;
let pypi_deps = convert_uv_requirements_txt_to_pep508(reqs_txt)?;

(vec![], pypi_deps)
}
};

workspace.add_specs(conda_deps, pypi_deps, &platforms, &feature_name)?;

match workspace
.workspace()
.environment(&EnvironmentName::from_str(&environment_string)?)
{
match workspace.workspace().environment(&environment_name) {
None => {
// add environment if it does not already exist
workspace.manifest().add_environment(
environment_string.clone(),
Some(vec![feature_string.clone()]),
environment_name.to_string(),
Some(vec![feature_name.to_string()]),
None,
true,
)?;
Expand All @@ -167,7 +241,7 @@ async fn import_conda_env(args: Args) -> miette::Result<()> {
let features = env
.features()
.map(|f| f.name.as_str().to_string())
.chain(std::iter::once(feature_string))
.chain(std::iter::once(feature_name.to_string()))
.collect();
Some(features)
};
Expand Down
Loading
Loading