Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,518 changes: 1,001 additions & 517 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ pixi-apps-backend-macros = { path = "crates/pixi-apps-backend-macros" }
pixi-apps-platform-tauri = { path = "crates/pixi-apps-platform-tauri" }
pixi-apps-platform-web = { path = "crates/pixi-apps-platform-web" }

pixi_api = { package = "pixi_api", git = "https://github.com/prefix-dev/pixi", tag = "v0.83.0" }
# TODO: Switch back to a pixi release tag once the run/shell API is released
pixi_api = { package = "pixi_api", git = "https://github.com/haecker-felix/pixi.git", branch = "pixi_api_run" }

axum = { version = "0.8", features = ["ws"] }
clap = { version = "4.5", features = ["derive"] }
Expand Down Expand Up @@ -56,6 +57,8 @@ tauri-plugin-single-instance = "2"
tokio = "1.47"
tokio-test = "0.4.5"
toml = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1.18", features = ["v4"] }
which = "8.0.0"
windows-sys = "0.61"
Expand All @@ -69,7 +72,7 @@ windows-sys = "0.61"
# applies this patch, but `[patch]` sections do not propagate through the
# `pixi_api` git dependency, so it has to be repeated here. Keep the tag in sync
# with `pixi_api`.
reqwest-middleware = { git = "https://github.com/prefix-dev/pixi", tag = "v0.83.0" }
reqwest-middleware = { git = "https://github.com/haecker-felix/pixi", branch = "pixi_api_run" }
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }

# Enable a small amount of optimization in the dev profile.
Expand Down
2 changes: 2 additions & 0 deletions crates/pixi-apps-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ tokio = { workspace = true, features = [
"sync",
"time",
] }
tracing.workspace = true
tracing-subscriber.workspace = true
which.workspace = true

[target.'cfg(target_os = "windows")'.dependencies]
Expand Down
33 changes: 19 additions & 14 deletions crates/pixi-apps-backend/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ use tokio::io::AsyncBufReadExt;
use tokio::process::Command;
use which::which;

use crate::{error::Error, event::EventPayload, pty::find_pixi_binary, session::Session, utils};
use crate::{
error::Error,
event::EventPayload,
pty::{PtyCommandInvocation, PtyInvocation, PtyInvocationKind},
session::Session,
utils,
};

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -267,20 +273,19 @@ pub async fn open_editor(
environment: String,
command: String,
) -> Result<(), Error> {
let pixi = find_pixi_binary();

let mut cmd = Command::new(pixi);

let invocation = PtyInvocation {
cwd: root.clone(),
manifest,
kind: PtyInvocationKind::Command(PtyCommandInvocation {
command: command.clone(),
environment: environment.clone(),
}),
};
let argv = invocation.argv()?;

let mut cmd = Command::new(&argv[0]);
cmd.current_dir(root.clone());

cmd.args([
"run",
"--manifest-path",
&manifest,
"--environment",
&environment,
&command,
]);
cmd.args(&argv[1..]);

cmd.stdin(Stdio::null())
.stdout(Stdio::null())
Expand Down
1 change: 1 addition & 0 deletions crates/pixi-apps-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod notification;
pub mod pixi;
pub mod pty;
pub mod router;
pub mod runner;
pub mod session;
pub mod settings;
pub mod state;
Expand Down
5 changes: 3 additions & 2 deletions crates/pixi-apps-backend/src/pixi/workspace/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::str::FromStr;

use indexmap::IndexSet;
use pixi_api::core::environment::LockFileUsage;
use pixi_api::core::workspace::DiscoveryStart;
use pixi_api::manifest::HasFeaturesIter;
use pixi_api::manifest::{EnvironmentName, FeatureName, PrioritizedChannel};
use pixi_api::manifest::{PixiPlatform, PixiPlatformName};
Expand All @@ -29,13 +30,13 @@ pub struct Environment {

#[command]
pub async fn root(session: Session, workspace: PathBuf) -> Result<PathBuf, Error> {
let workspace = utils::workspace(workspace).await?;
let workspace = utils::workspace(DiscoveryStart::SearchRoot(workspace)).await?;
Ok(workspace.root().to_path_buf())
}

#[command]
pub async fn manifest(session: Session, workspace: PathBuf) -> Result<PathBuf, Error> {
let workspace = utils::workspace(workspace).await?;
let workspace = utils::workspace(DiscoveryStart::SearchRoot(workspace)).await?;
Ok(workspace.workspace.provenance.absolute_path())
}

Expand Down
101 changes: 12 additions & 89 deletions crates/pixi-apps-backend/src/pty.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::VecDeque, ffi::OsString, io::Write};
Expand All @@ -13,7 +12,7 @@ use tokio::time::timeout;

use pixi_apps_backend_macros::command;

use crate::{error::Error, event::EventPayload, session::Session};
use crate::{error::Error, event::EventPayload, runner::RUNNER_ARG, session::Session};

/// How the PTY process was terminated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -26,52 +25,6 @@ enum TerminationKind {
Killed,
}

/// Find the pixi binary with fallback locations.
///
/// Search order:
/// 1. System PATH (using `which`)
/// 2. `$PIXI_HOME/bin/pixi` if PIXI_HOME is set
/// 3. `~/.pixi/bin/pixi` as last resort
///
/// Returns the full path if found, otherwise falls back to "pixi" for PATH resolution.
pub(crate) fn find_pixi_binary() -> String {
// 1. Check PATH first
if let Ok(path) = which::which("pixi") {
return path.to_string_lossy().into_owned();
}

// 2. Check $PIXI_HOME/bin/pixi if PIXI_HOME is set
if let Ok(pixi_home) = std::env::var("PIXI_HOME") {
let pixi_path = PathBuf::from(&pixi_home).join("bin").join("pixi");
if pixi_path.is_file() {
return pixi_path.to_string_lossy().into_owned();
}
}

// 3. Try ~/.pixi/bin/pixi as last resort
if let Some(home) = home_dir() {
let pixi_path = home.join(".pixi").join("bin").join("pixi");
if pixi_path.is_file() {
return pixi_path.to_string_lossy().into_owned();
}
}

// Fall back to "pixi" and let the system resolve it
"pixi".into()
}

/// Get the user's home directory.
fn home_dir() -> Option<PathBuf> {
#[cfg(windows)]
{
std::env::var("USERPROFILE").ok().map(PathBuf::from)
}
#[cfg(not(windows))]
{
std::env::var("HOME").ok().map(PathBuf::from)
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PtyInvocation {
pub cwd: String,
Expand Down Expand Up @@ -106,46 +59,16 @@ pub struct PtyCommandInvocation {
}

impl PtyInvocation {
pub fn argv(&self) -> Vec<String> {
let pixi = find_pixi_binary();
match &self.kind {
PtyInvocationKind::Shell(data) => {
vec![
pixi,
"shell".into(),
"--manifest-path".into(),
self.manifest.clone(),
"--environment".into(),
data.environment.clone(),
]
}
PtyInvocationKind::Task(data) => {
let mut argv = vec![
pixi,
"run".into(),
"--manifest-path".into(),
self.manifest.clone(),
];
if let Some(env) = &data.environment {
argv.push("--environment".into());
argv.push(env.clone());
}
argv.push(data.task.clone());
argv.extend(data.args.clone());
argv
}
PtyInvocationKind::Command(data) => {
vec![
pixi,
"run".into(),
"--manifest-path".into(),
self.manifest.clone(),
"--environment".into(),
data.environment.clone(),
data.command.clone(),
]
}
}
/// The command line that starts the runner for this invocation
pub fn argv(&self) -> Result<Vec<String>> {
let exe = std::env::current_exe()
.map_err(|err| miette::miette!("failed to locate the app executable: {err}"))?;
let invocation = serde_json::to_string(self).into_diagnostic()?;
Ok(vec![
exe.to_string_lossy().into_owned(),
RUNNER_ARG.into(),
invocation,
])
}
}

Expand Down Expand Up @@ -245,7 +168,7 @@ impl PtyHandle {
.openpty(size)
.map_err(|err| miette::miette!("failed to open PTY: {err}"))?;

let argv = invocation.argv().into_iter().map(OsString::from).collect();
let argv = invocation.argv()?.into_iter().map(OsString::from).collect();
let mut command = CommandBuilder::from_argv(argv);
command.env("TERM", "xterm-256color");
command.cwd(invocation.cwd.clone());
Expand Down
112 changes: 112 additions & 0 deletions crates/pixi-apps-backend/src/runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::{
pty::{PtyInvocation, PtyInvocationKind},
utils,
};
use miette::IntoDiagnostic;
use pixi_api::{
Interface, WorkspaceContext,
core::workspace::DiscoveryStart,
reporters::TopLevelProgress,
workspace::{RunOptions, ShellOptions},
};

/// Starts the app as runner, followed by the [`PtyInvocation`] as JSON
pub const RUNNER_ARG: &str = "--runner";

/// Runs the invocation and exits, if the app was started as runner
pub fn run_and_exit_if_runner() {
let mut args = std::env::args().skip(1);
if args.next().as_deref() != Some(RUNNER_ARG) {
return;
}

// Like "pixi run", show warnings and the task headers
tracing_subscriber::fmt()
.with_max_level(tracing::Level::WARN)
.with_writer(std::io::stderr)
.without_time()
.init();

let runtime = tokio::runtime::Runtime::new().expect("Failed to start the tokio runtime");

let result = runtime.block_on(async move {
let json = args.next().unwrap_or_default();
let invocation: PtyInvocation = serde_json::from_str(&json).into_diagnostic()?;

// Retrieve workspace context
let start = DiscoveryStart::ExplicitManifest(invocation.manifest.into());
let workspace = utils::workspace(start).await.map_err(|err| err.0)?;
let progress = TopLevelProgress::from_global();
let context = WorkspaceContext::new(RunnerInterface, workspace).with_progress(progress);

// Construct RunOptions / ShellOptions and run the task, command or shell
match invocation.kind {
PtyInvocationKind::Shell(data) => {
let options = ShellOptions {
environment: Some(data.environment.parse().into_diagnostic()?),
..Default::default()
};
context.shell(options).await
}
PtyInvocationKind::Task(data) => {
let options = RunOptions {
task: std::iter::once(data.task).chain(data.args).collect(),
environment: data
.environment
.map(|name| name.parse())
.transpose()
.into_diagnostic()?,
..Default::default()
};
context.run(options).await
}
PtyInvocationKind::Command(data) => {
let options = RunOptions {
task: vec![data.command],
environment: Some(data.environment.parse().into_diagnostic()?),
..Default::default()
};
context.run(options).await
}
}
});

let code = result.unwrap_or_else(|err| {
eprintln!("{err:?}");
1
});
std::process::exit(code);
}

/// Writes messages to the terminal the runner runs in
struct RunnerInterface;

impl Interface for RunnerInterface {
async fn is_cli(&self) -> bool {
false
}

async fn confirm(&self, _msg: &str) -> miette::Result<bool> {
Ok(false)
}

async fn select(&self, _msg: &str, _options: &[String]) -> miette::Result<Option<usize>> {
Ok(None)
}

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

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

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

async fn error(&self, msg: &str) {
eprintln!("{msg}");
}
}
5 changes: 5 additions & 0 deletions crates/pixi-apps-backend/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ impl Interface for Session {
Ok(self.request_confirm("Confirm", &message, "Yes", "No").await)
}

async fn select(&self, _msg: &str, _options: &[String]) -> Result<Option<usize>> {
// TODO: Implement select dialog in frontend, once needed
Ok(None)
}

async fn info(&self, msg: &str) {
self.send_event(EventPayload::Notification(Notification {
level: NotificationLevel::Info,
Expand Down
6 changes: 3 additions & 3 deletions crates/pixi-apps-backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ where
.map_err(|e| E::from(format!("Execution failed: {}", e)))?
}

pub async fn workspace(workspace: PathBuf) -> Result<Workspace, Error> {
pub async fn workspace(start: DiscoveryStart) -> Result<Workspace, Error> {
let workspace_result = WorkspaceLocator::for_cli()
.with_consider_environment(false)
.with_search_start(DiscoveryStart::SearchRoot(workspace))
.with_search_start(start)
.locate()
.await;

Expand All @@ -48,7 +48,7 @@ pub async fn workspace_context(
session: Session,
path: PathBuf,
) -> Result<WorkspaceContext<Session>, Error> {
let workspace = workspace(path).await?;
let workspace = workspace(DiscoveryStart::SearchRoot(path)).await?;

Ok(WorkspaceContext::new(session, workspace))
}
Expand Down
Loading
Loading