Skip to content

Conversation

@haecker-felix
Copy link
Collaborator

@haecker-felix haecker-felix commented Sep 9, 2025

High Level Goal:

┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│    CLI          │  │  Integration    │  │      UI         │
│                 │  │     Tests       │  │                 │
└─────────────────┘  └─────────────────┘  └─────────────────┘
         │                     │                     │
         └─────────────────────┼─────────────────────┘
                               │
                    ┌─────────────────┐
                    │    pixi_api     │ 
                    │                 │
                    └─────────────────┘
                               │
                    ┌─────────────────┐
                    │    pixi_core    │ 
                    │                 │
                    └─────────────────┘

For this PR I'll limit myself to pixi init and pixi reinstall.

Proposal

  • Reuse already existing CLI Arg structs, refactor them into *Options structs (e.g. InitOptions)
    • Add some builder pattern for them, so you can use them more nicely (e.g. by using derive_builder crate) (?TODO?)
    • The clap specific macros could be gated behind a cli feature flag, so non-cli consumers don't get clap as dependency (?TODO?)
    • This way we would get an API which is very close to what's already possible with the CLI interface
      • This should not prevent us from adding further APIs in the future that are not exposed by the Pixi CLI, if that becomes necessary.

-> No, turned out that it is better to use separate option structs which are fully independent of the CLI Arg structs.

Next step is taking care of the CLI execute() functions. A major challenge is that the CLI execute() functions not only contain CLI-specific code, but also large parts of the business logic itself.

First, I tried to strictly separate the business logic from the user interactions (for the example pixi init). But I have the feeling that this is not the right approach.

  • We would have to refactor/reimplement large parts of Pixi - I'm not sure if that's realistic / good.
  • The current CLI implementation (+ logic) was developed based on the assumption that feedback can be provided to the user anytime (eprintln!(...)), same with interactions (dialoguer)
    • (something which is perfectly fine, given pixi is a CLI application!)
  • As a result, the business logic is closely intertwined with user interaction in some places
  • I am concerned that strict separation would also result in a lot of duplicate code, since many things remain the same regardless of whether they are executed by a CLI or displayed in a GUI
    • The only part which would differ is very likely how the interaction with the user happens
  • ... and difficult to implement in some places, as a feature can lead to a series of interactions or progress reporting (e.g., multiple progress bars displayed simultaneously).

Alternatively (what this PR does), we can also simply use the existing logic as it is and replace the CLI-specific parts with a generic Interface trait. The specific implementation of the Interface trait can then determine whether and how messages, interactions, and similar are handled / presented to the user.

Example for pixi init:

pub(crate) async fn init<I: Interface>(interface: &I, options: InitOptions) -> miette::Result<()> {

Very barebone/simple example for the CLI interface (very likely not sufficient yet, probably needs more context so the Interface implementors can display this in a useful way to the user):

pub struct CliInterface {}

impl Interface for CliInterface {
    fn is_cli(&self) -> bool {
        true
    }

    fn confirm(&self, msg: &str) -> miette::Result<bool> {
        dialoguer::Confirm::new()
            .with_prompt(msg)
            .default(false)
            .show_default(true)
            .interact()
            .into_diagnostic()
    }

    fn message(&self, msg: &str) {
        eprintln!("{msg}",);
    }

    fn success(&self, msg: &str) {
        eprintln!("{}{msg}", console::style(console::Emoji("✔ ", "")).green(),);
    }

    fn warning(&self, msg: &str) {
        eprintln!(
            "{}{msg}",
            console::style(console::Emoji("⚠️ ", "")).yellow(),
        );
    }

    fn error(&self, msg: &str) {
        eprintln!(
            "{}{msg}",
            console::style(console::Emoji("❌ ", "")).yellow(),
        );    
    }
}
  • Even for the CLI alone, this would have the advantage that the user interaction is neatly abstracted and consistent styling is used everywhere
  • No separation between CLI/GUI implementation, the actual logic only needs to be maintained once
  • And this would then also allow all consumers of the Interface trait to benefit directly from potential pixi_api improvements / fixes

@haecker-felix haecker-felix self-assigned this Sep 9, 2025
@haecker-felix haecker-felix marked this pull request as draft September 9, 2025 13:08
@haecker-felix
Copy link
Collaborator Author

After sleeping on it for another night and giving it some more thought, here are some points that could become problematic:

  • Some Args contain stuff which is only relevant for CLI, for example run::Args has a h field (=help)
  • Not sure if we can / if it makes sense to abstract everything behind the generic Interface trait
    • There are use cases where you need the actual underlying structs from pixi_api and not just some abstracted interface representation
      • an actual Vec<TaskInfo> vs. a generic abstracted list of task names Vec<String>
    • There may be more complex data which may be difficult to abstract with the generic Interface trait (e.g. lists, dependency trees, ...)

@Hofer-Julian
Copy link
Contributor

Some Args contain stuff which is only relevant for CLI

As far as I can tell, you are nearly there:

pub(crate) async fn init<I: Interface>(interface: &I, options: InitOptions) -> miette::Result<()> {

This looks good, but options should be a struct that doesn't implement clap::Parser and only contains the necessary fields.

For the CLI, you then have a separate struct that implements clap::Parser and From for the struct options Struct.

@tdejager
Copy link
Contributor

@Hofer-Julian @haecker-felix This looks good! Note that this is also basically what the PixiControl does in a lot of cases except that it kind of builds with an .await might be good to unify that with the changes that are made here, otherwise we have somewhat duplicated code. I think you could make the PixiControl and builder use this under the hood.

@haecker-felix haecker-felix changed the title feat: Add new pixi_api crate [WIP] feat: Add new pixi_api crate Sep 22, 2025
@haecker-felix
Copy link
Collaborator Author

This looks good! Note that this is also basically what the PixiControl does in a lot of cases except that it kind of builds with an .await might be good to unify that with the changes that are made here, otherwise we have somewhat duplicated code.

Yes, that's true. I tried to use pixi_api under the hood for PixiControl, but aborted it since you would need to change pretty much code. Long term, I think it should be possible to replace PixiControl entirely with pixi_api / ApiContext - but right now it's too early, since ApiContext doesn't cover everything yet which is needed by the integration tests.

I think it's better to wait till ApiContext covers everything needed, and then decide whether we'll be able to replace PixiControl entirely (or to make use of ApiContext under-the-hood). Integrating it right now partially is pretty tricky.


Overview how we can abstract the current CLI information/data via pixi_api:

  • stderr: Gets replaced entirely by using the new Interface trait (covered in this PR for pixi reinstall and pixi init)
  • stdout: The pixi_api crate would return the raw structs (eg. TaskInfo for pixi task list), and the implementation (CLI/UI) would decide/control how to display them (not covered by this PR)
    • CLI would print the table as is
    • Some UI could list the tasks in some grid, for example
  • indicatif progress bars: Currently not abstracted, I think needs larger code/abstraction changes around reporters / TopLevelProgress. For now, UIs would need to display spinners or similar when it's some operation which needs more time.

@haecker-felix haecker-felix marked this pull request as ready for review September 22, 2025 15:07
@haecker-felix haecker-felix changed the title feat: Add new pixi_api crate feat: Add new pixi_api abstraction crate Sep 22, 2025
@haecker-felix
Copy link
Collaborator Author

Self-Review: Do we need the *Options structs at all? For example, in case of ReinstallOptions we probably could make the reinstall_packages and reinstall_environments fields a parameter of the reinstall function.

But in the case of InitOptions it probably would be too much to add all fields as function parameter?

@haecker-felix
Copy link
Collaborator Author

haecker-felix commented Sep 23, 2025

Small refactor:

  • ApiContext is now WorkspaceContext
  • Global stuff would get its own GlobalContext struct
  • Everything which is related to WorkspaceContext is now in the workspace mod
  • This makes the API a little bit more nice to use, since the WorkspaceContext now holds a pixi_core::Workspace which can be referenced.
  • init() now returns the initialized Workspace struct

And I ported pixi workspace name to pixi_api as well, so it's possible to retrieve / change the workspace name.

@haecker-felix haecker-felix changed the title feat: Add new pixi_api abstraction crate feat: Add new (minimal) pixi_api abstraction crate Sep 23, 2025
Copy link
Contributor

@Hofer-Julian Hofer-Julian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few comments, but it looks good in general!

Comment on lines 27 to 32
workspace
.workspace
.value
.workspace
.name
.expect("workspace name must have been set")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use name here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you mean display_name()?

Copy link
Contributor

@Hofer-Julian Hofer-Julian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@Hofer-Julian Hofer-Julian merged commit 8693cae into prefix-dev:main Sep 23, 2025
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants