Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ clap = { version = "4", features = ["derive"] }
# Web server
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
futures = "0.3"
async-stream = "0.3"

# Serialization
serde = { version = "1", features = ["derive"] }
Expand Down
8 changes: 7 additions & 1 deletion app/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ pub fn create_routes(frontend_dir: Option<&str>) -> Router<Arc<AppState>> {
.route("/about", get(health::about))
.route("/projects/validate", post(projects::validate_project))
.route("/projects/recent", get(projects::get_recent_projects))
.route("/xcode/discover", post(xcode::discover_project));
.route("/xcode/discover", post(xcode::discover_project))
.route("/xcode/build", post(xcode::build_scheme))
.route("/xcode/build/stream", post(xcode::build_scheme_stream))
.route(
"/xcode/launchable-products",
post(xcode::get_launchable_products),
);

let router = Router::new().nest("/api", api_routes);

Expand Down
86 changes: 85 additions & 1 deletion app/src/routes/xcode.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use crate::xcode;
use axum::{http::StatusCode, response::IntoResponse, Json};
use axum::{
http::StatusCode,
response::{
sse::{Event, KeepAlive, Sse},
IntoResponse,
},
Json,
};
use futures::stream::StreamExt;
use serde::Deserialize;
use serde_json::json;
use std::path::Path;
Expand All @@ -9,6 +17,17 @@ pub struct DiscoverProjectRequest {
pub path: String,
}

#[derive(Debug, Deserialize)]
pub struct BuildSchemeRequest {
pub path: String,
pub scheme: String,
}

#[derive(Debug, Deserialize)]
pub struct GetLaunchableProductsRequest {
pub build_dir: String,
}

/// Discover Xcode project information (schemes, targets, configurations)
pub async fn discover_project(Json(request): Json<DiscoverProjectRequest>) -> impl IntoResponse {
let path = Path::new(&request.path);
Expand All @@ -24,3 +43,68 @@ pub async fn discover_project(Json(request): Json<DiscoverProjectRequest>) -> im
.into_response(),
}
}

/// Build an Xcode scheme for iOS Simulator with code signing disabled
pub async fn build_scheme(Json(request): Json<BuildSchemeRequest>) -> impl IntoResponse {
let path = Path::new(&request.path);

match xcode::build_scheme(path, &request.scheme).await {
Ok(result) => (StatusCode::OK, Json(serde_json::to_value(result).unwrap())).into_response(),
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The unwrap() call will panic if serialization fails. While serialization of the BuildResult is unlikely to fail, for consistency with error handling patterns in the rest of the code, consider using map_err to convert to a 500 Internal Server Error, or at least use expect() with a descriptive message.

Suggested change
Ok(result) => (StatusCode::OK, Json(serde_json::to_value(result).unwrap())).into_response(),
Ok(result) => match serde_json::to_value(result) {
Ok(value) => (StatusCode::OK, Json(value)).into_response(),
Err(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": error.to_string() })),
)
.into_response(),
},

Copilot uses AI. Check for mistakes.
Err(error) => (
StatusCode::BAD_REQUEST,
Json(json!({ "error": error.to_string() })),
)
.into_response(),
}
}

/// Get launchable products from a build directory
pub async fn get_launchable_products(
Json(request): Json<GetLaunchableProductsRequest>,
) -> impl IntoResponse {
match xcode::get_launchable_products_from_dir(&request.build_dir).await {
Ok(products) => (
StatusCode::OK,
Json(serde_json::to_value(products).unwrap()),
)
.into_response(),
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The unwrap() call will panic if serialization fails. Consider using map_err to handle potential serialization errors gracefully, or at least use expect() with a descriptive message for clarity.

Suggested change
Ok(products) => (
StatusCode::OK,
Json(serde_json::to_value(products).unwrap()),
)
.into_response(),
Ok(products) => match serde_json::to_value(products) {
Ok(value) => (
StatusCode::OK,
Json(value),
)
.into_response(),
Err(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("Failed to serialize products: {}", error) })),
)
.into_response(),
},

Copilot uses AI. Check for mistakes.
Err(error) => (
StatusCode::BAD_REQUEST,
Json(json!({ "error": error.to_string() })),
)
.into_response(),
}
}

/// Stream build output via Server-Sent Events
pub async fn build_scheme_stream(
Json(request): Json<BuildSchemeRequest>,
) -> Result<
Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>>,
(StatusCode, Json<serde_json::Value>),
> {
let path = Path::new(&request.path);

let event_stream = match xcode::build_scheme_stream(path, &request.scheme).await {
Ok(stream) => stream,
Err(error) => {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({ "error": error.to_string() })),
));
}
};

let sse_stream = event_stream.map(|result| match result {
Ok(event) => {
let json_data = serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_string());
Ok(Event::default().data(json_data))
}
Err(_) => {
let error_json = json!({"type": "error", "message": "Stream error"}).to_string();
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The unwrap_or_else with a default empty JSON object is appropriate, but the error case on line 103 silently swallows the build error details. Consider including the error message in the SSE event so clients can distinguish between serialization errors and actual build errors, such as including the error string in the error event data.

Suggested change
Err(_) => {
let error_json = json!({"type": "error", "message": "Stream error"}).to_string();
Err(error) => {
let error_json = json!({
"type": "error",
"message": error.to_string()
})
.to_string();

Copilot uses AI. Check for mistakes.
Ok(Event::default().data(error_json))
}
});

Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
Loading