use std::{collections::BTreeSet, fmt, future::Future, pin::Pin};
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use crate::{
artifact::ArtifactManifest,
cancellation::CancellationToken,
error::{RemoteBuildError, RemoteBuildResult},
protocol::{
CURRENT_PROTOCOL_VERSION, CleanupConfirmation, IosArtifactType, IosDeviceBuildRequest,
JobState, ProtocolPath, ProtocolPathSemantics, ProtocolVersion, RemoteBuildEvent,
validate_identifier,
},
signing::SigningMode,
source::SourceMode,
};
pub type ProviderFuture<'a, T> = Pin<Box<dyn Future<Output = RemoteBuildResult<T>> + Send + 'a>>;
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "feature", content = "value")]
pub enum ProviderFeature {
SourceMode(SourceMode),
IosDeviceBuild,
IosSimulatorBuild,
SigningMode(SigningMode),
PersonalTeam,
LiveEvents,
LiveLogs,
Cancellation,
ArtifactType(IosArtifactType),
Cache,
ArtifactListing,
ArtifactDownload,
Cleanup,
PhysicalDeviceAccess,
}
impl fmt::Display for ProviderFeature {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SourceMode(mode) => write!(formatter, "source mode `{mode:?}`"),
Self::IosDeviceBuild => formatter.write_str("physical-iPhone builds"),
Self::IosSimulatorBuild => formatter.write_str("iOS Simulator builds"),
Self::SigningMode(mode) => write!(formatter, "signing mode `{mode:?}`"),
Self::PersonalTeam => formatter.write_str("Apple Personal Team signing"),
Self::LiveEvents => formatter.write_str("live build events"),
Self::LiveLogs => formatter.write_str("live logs"),
Self::Cancellation => formatter.write_str("job cancellation"),
Self::ArtifactType(kind) => write!(formatter, "artifact type `{kind}`"),
Self::Cache => formatter.write_str("provider build cache"),
Self::ArtifactListing => formatter.write_str("artifact listing"),
Self::ArtifactDownload => formatter.write_str("artifact download"),
Self::Cleanup => formatter.write_str("remote cleanup"),
Self::PhysicalDeviceAccess => formatter.write_str("physical-device access"),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ProviderCapabilities {
pub source_modes: BTreeSet<SourceMode>,
pub ios_device_build: bool,
pub ios_simulator_build: bool,
pub signing_modes: BTreeSet<SigningMode>,
pub personal_team: bool,
pub live_events: bool,
pub live_logs: bool,
pub cancellation: bool,
pub artifact_types: BTreeSet<IosArtifactType>,
pub cache: bool,
pub max_source_bytes: Option<u64>,
pub retention_seconds: Option<u64>,
pub artifact_listing: bool,
pub artifact_download: bool,
pub cleanup: bool,
pub physical_device_access: bool,
}
impl ProviderCapabilities {
pub fn supports(&self, feature: &ProviderFeature) -> bool {
match feature {
ProviderFeature::SourceMode(mode) => self.source_modes.contains(mode),
ProviderFeature::IosDeviceBuild => self.ios_device_build,
ProviderFeature::IosSimulatorBuild => self.ios_simulator_build,
ProviderFeature::SigningMode(mode) => self.signing_modes.contains(mode),
ProviderFeature::PersonalTeam => self.personal_team,
ProviderFeature::LiveEvents => self.live_events,
ProviderFeature::LiveLogs => self.live_logs,
ProviderFeature::Cancellation => self.cancellation,
ProviderFeature::ArtifactType(kind) => self.artifact_types.contains(kind),
ProviderFeature::Cache => self.cache,
ProviderFeature::ArtifactListing => self.artifact_listing,
ProviderFeature::ArtifactDownload => self.artifact_download,
ProviderFeature::Cleanup => self.cleanup,
ProviderFeature::PhysicalDeviceAccess => self.physical_device_access,
}
}
pub fn require(
&self,
provider: impl Into<String>,
feature: ProviderFeature,
) -> RemoteBuildResult<()> {
if self.supports(&feature) {
Ok(())
} else {
Err(RemoteBuildError::UnsupportedCapability {
provider: provider.into(),
feature,
})
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct HandshakeRequest {
pub protocol_version: ProtocolVersion,
pub client_version: Version,
pub required_features: Vec<ProviderFeature>,
}
impl HandshakeRequest {
pub fn validate(&self) -> RemoteBuildResult<()> {
CURRENT_PROTOCOL_VERSION
.negotiate(self.protocol_version)
.map(|_| ())
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct HandshakeResponse {
pub protocol_version: ProtocolVersion,
pub worker_version: Version,
pub provider: String,
pub worker_id: String,
pub capabilities: ProviderCapabilities,
}
impl HandshakeResponse {
pub fn negotiate(
request: &HandshakeRequest,
provider: impl Into<String>,
worker_id: impl Into<String>,
worker_version: Version,
capabilities: ProviderCapabilities,
) -> RemoteBuildResult<Self> {
let protocol_version = CURRENT_PROTOCOL_VERSION.negotiate(request.protocol_version)?;
let response = Self {
protocol_version,
worker_version,
provider: provider.into(),
worker_id: worker_id.into(),
capabilities,
};
response.validate_for(request)?;
Ok(response)
}
pub fn validate_for(&self, request: &HandshakeRequest) -> RemoteBuildResult<()> {
request.validate()?;
request
.protocol_version
.negotiate(self.protocol_version)
.map(|_| ())?;
validate_identifier("provider", &self.provider)?;
validate_identifier("worker_id", &self.worker_id)?;
for feature in &request.required_features {
self.capabilities
.require(self.provider.clone(), feature.clone())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct ProviderDoctorRequest {
pub protocol_version: ProtocolVersion,
pub operation_id: String,
pub require_signing: bool,
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ProviderCheckStatus {
Ready,
Warning,
Error,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct ProviderCheck {
pub code: String,
pub status: ProviderCheckStatus,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct ProviderDoctorReport {
pub protocol_version: ProtocolVersion,
pub provider: String,
pub ready: bool,
pub checks: Vec<ProviderCheck>,
pub capabilities: ProviderCapabilities,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct JobHandle {
pub job_id: String,
pub state: JobState,
pub created_at_ms: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct EventRequest {
pub job_id: String,
pub after_sequence: Option<u64>,
pub limit: u32,
}
impl EventRequest {
pub fn validate(&self) -> RemoteBuildResult<()> {
validate_identifier("job_id", &self.job_id)?;
if !(1..=1000).contains(&self.limit) {
return Err(RemoteBuildError::InvalidEventPayload {
event: "event_request",
reason: "limit must be between 1 and 1000",
});
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
pub struct EventPage {
pub job_id: String,
pub state: JobState,
pub events: Vec<RemoteBuildEvent>,
pub next_sequence: Option<u64>,
pub complete: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct CancellationRequest {
pub job_id: String,
pub reason: String,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct CancellationAck {
pub job_id: String,
pub accepted: bool,
pub state: JobState,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct ArtifactListRequest {
pub job_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct ArtifactDownloadRequest {
pub job_id: String,
pub artifact_id: String,
pub destination: ProtocolPath,
}
impl ArtifactDownloadRequest {
pub fn validate(&self) -> RemoteBuildResult<()> {
validate_identifier("job_id", &self.job_id)?;
validate_identifier("artifact_id", &self.artifact_id)?;
self.destination.validate()?;
if self.destination.semantics != ProtocolPathSemantics::ClientAbsolute {
return Err(RemoteBuildError::InvalidEventPayload {
event: "artifact_download_request",
reason: "destination must be client_absolute",
});
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
pub struct ArtifactDownloadResult {
pub manifest: ArtifactManifest,
pub local_path: ProtocolPath,
pub local_file_identity: String,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct CleanupRequest {
pub job_id: String,
pub remove_artifacts: bool,
}
pub trait BuildProvider: Send + Sync {
fn id(&self) -> &str;
fn capabilities(&self) -> &ProviderCapabilities;
fn handshake(
&self,
request: HandshakeRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, HandshakeResponse>;
fn doctor(
&self,
request: ProviderDoctorRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, ProviderDoctorReport>;
fn submit(
&self,
request: IosDeviceBuildRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, JobHandle>;
fn events(
&self,
request: EventRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, EventPage>;
fn cancel(
&self,
_request: CancellationRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, CancellationAck> {
let result = cancellation.check().and_then(|()| {
self.capabilities()
.require(self.id().to_owned(), ProviderFeature::Cancellation)
.and_then(|()| {
Err(RemoteBuildError::UnsupportedCapability {
provider: self.id().to_owned(),
feature: ProviderFeature::Cancellation,
})
})
});
Box::pin(async move { result })
}
fn list_artifacts(
&self,
_request: ArtifactListRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, Vec<ArtifactManifest>> {
let result = cancellation.check().and_then(|()| {
Err(RemoteBuildError::UnsupportedCapability {
provider: self.id().to_owned(),
feature: ProviderFeature::ArtifactListing,
})
});
Box::pin(async move { result })
}
fn download_artifact(
&self,
_request: ArtifactDownloadRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, ArtifactDownloadResult> {
let result = cancellation.check().and_then(|()| {
Err(RemoteBuildError::UnsupportedCapability {
provider: self.id().to_owned(),
feature: ProviderFeature::ArtifactDownload,
})
});
Box::pin(async move { result })
}
fn cleanup(
&self,
_request: CleanupRequest,
cancellation: CancellationToken,
) -> ProviderFuture<'_, CleanupConfirmation> {
let result = cancellation.check().and_then(|()| {
Err(RemoteBuildError::UnsupportedCapability {
provider: self.id().to_owned(),
feature: ProviderFeature::Cleanup,
})
});
Box::pin(async move { result })
}
}