#![allow(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::too_many_lines
)]
use std::collections::BTreeMap;
use std::fmt;
use std::path::Path;
use presolve_compiler::persistent_cache::CacheReportSelector;
use presolve_compiler::platform::{
derive_workspace_id_v1, ContractVersion, RequestedCompilationMode, WorkspaceInput,
WorkspaceSnapshot, WorkspaceSource,
};
use presolve_compiler::service::{
CompileRequest, CompilerServiceHost, CompleteSource, IncrementalReportSelector,
WatchCandidateV1, WatchChangeBatchV1, WorkspaceCompileRequestV1,
WorkspacePackageCompileRequestV1,
};
use presolve_compiler::watch::{
ObservedChangeV1, WatchDebounceV1, WatchSessionConfigurationV1, WATCH_CHANGE_BATCH_V1_SCHEMA,
WATCH_SESSION_CONFIGURATION_V1_SCHEMA,
};
use presolve_compiler::workspace::{
WorkspaceManifestV1, WorkspacePackageDescriptorV1, WorkspacePolicyV1,
WORKSPACE_MANIFEST_V1_SCHEMA,
};
use crate::{
load_explicit_project_envelope_v1, load_explicit_source_inputs_v1, CliExplicitSourceSpecV1,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliWorkspaceResultV1 {
pub workspace_id: String,
pub status: String,
pub manifest_identity: String,
pub graph_identity: String,
pub plan_identity: String,
pub package_snapshot_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliWorkspaceErrorV1 {
pub code: &'static str,
pub message: String,
}
impl fmt::Display for CliWorkspaceErrorV1 {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(output, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for CliWorkspaceErrorV1 {}
fn compiler_contract() -> ContractVersion {
ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION")))
}
pub fn run_explicit_workspace_v1(
configuration_path: &Path,
source_specs: &[CliExplicitSourceSpecV1],
verify_clean_equivalence: bool,
) -> Result<CliWorkspaceResultV1, CliWorkspaceErrorV1> {
let project_root = configuration_path.parent().ok_or(CliWorkspaceErrorV1 {
code: "L9F001_CONFIGURATION_PATH_INVALID",
message: "configuration path must have a parent directory".into(),
})?;
let envelope =
load_explicit_project_envelope_v1(project_root, configuration_path).map_err(|error| {
CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
}
})?;
let sources =
load_explicit_source_inputs_v1(&envelope.project_root, source_specs).map_err(|error| {
CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
}
})?;
let contract = compiler_contract();
let workspace_id =
derive_workspace_id_v1(&envelope.configuration).map_err(|error| CliWorkspaceErrorV1 {
code: "L9F002_INVALID_CONFIGURATION",
message: error.message,
})?;
let snapshot = WorkspaceSnapshot::from_input(&WorkspaceInput {
configuration: envelope.configuration.clone(),
sources: sources
.iter()
.map(|source| WorkspaceSource {
path: source.logical_path.clone(),
source: source.content.clone(),
language: None,
})
.collect(),
compiler_contract: contract.clone(),
})
.map_err(|error| CliWorkspaceErrorV1 {
code: "L9F003_INVALID_COMPLETE_CANDIDATE",
message: error.message,
})?;
let mut service = CompilerServiceHost::start(envelope.project_root.join(".presolve"), contract)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let session_id = service
.open_session(envelope.configuration.clone(), &workspace_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let manifest = WorkspaceManifestV1 {
schema: WORKSPACE_MANIFEST_V1_SCHEMA.into(),
version: 1,
workspace_id: workspace_id.to_string(),
packages: vec![WorkspacePackageDescriptorV1 {
package_id: "project".into(),
session_id: session_id.clone(),
display_name: Some("project".into()),
configuration_identity_hint: None,
metadata: BTreeMap::new(),
}],
dependencies: Vec::new(),
policy: WorkspacePolicyV1 {
failure_mode: "fail_fast".into(),
execution_mode: "deterministic_serial".into(),
result_detail: "summary".into(),
},
};
let request = CompileRequest {
configuration: envelope.configuration,
candidate_snapshot: snapshot,
sources: sources
.into_iter()
.map(|source| CompleteSource {
path: source.logical_path,
source: source.content,
language: None,
})
.collect(),
mode: RequestedCompilationMode::Automatic,
incremental_report: IncrementalReportSelector::Summary,
verify_exact_equivalence: verify_clean_equivalence,
cache_report: CacheReportSelector::Summary,
};
let result = service
.compile_workspace_v1(WorkspaceCompileRequestV1 {
manifest,
packages: vec![WorkspacePackageCompileRequestV1 {
package_id: "project".into(),
expected_commit_sequence: 0,
request,
}],
operation_id: "presolve-cli-workspace-v1".into(),
})
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
service
.verify_workspace(&result.workspace_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
service
.close_session(&session_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
Ok(CliWorkspaceResultV1 {
workspace_id: result.workspace_id,
status: result.status,
manifest_identity: result.manifest_identity,
graph_identity: result.graph_identity,
plan_identity: result.plan_identity,
package_snapshot_id: result
.package_results
.into_iter()
.next()
.and_then(|item| item.snapshot_id),
})
}
pub fn run_explicit_watch_once_v1(
configuration_path: &Path,
source_specs: &[CliExplicitSourceSpecV1],
) -> Result<String, CliWorkspaceErrorV1> {
let project_root = configuration_path.parent().ok_or(CliWorkspaceErrorV1 {
code: "L9F001_CONFIGURATION_PATH_INVALID",
message: "configuration path must have a parent directory".into(),
})?;
let envelope =
load_explicit_project_envelope_v1(project_root, configuration_path).map_err(|error| {
CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
}
})?;
let sources =
load_explicit_source_inputs_v1(&envelope.project_root, source_specs).map_err(|error| {
CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
}
})?;
let contract = compiler_contract();
let workspace_id =
derive_workspace_id_v1(&envelope.configuration).map_err(|error| CliWorkspaceErrorV1 {
code: "L9F002_INVALID_CONFIGURATION",
message: error.message,
})?;
let snapshot = WorkspaceSnapshot::from_input(&WorkspaceInput {
configuration: envelope.configuration.clone(),
sources: sources
.iter()
.map(|source| WorkspaceSource {
path: source.logical_path.clone(),
source: source.content.clone(),
language: None,
})
.collect(),
compiler_contract: contract.clone(),
})
.map_err(|error| CliWorkspaceErrorV1 {
code: "L9F003_INVALID_COMPLETE_CANDIDATE",
message: error.message,
})?;
let mut service = CompilerServiceHost::start(envelope.project_root.join(".presolve"), contract)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let session_id = service
.open_session(envelope.configuration.clone(), &workspace_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let watch_session_id = format!("cli-watch:{workspace_id}");
service
.create_watch_session(WatchSessionConfigurationV1 {
schema: WATCH_SESSION_CONFIGURATION_V1_SCHEMA.into(),
version: 1,
watch_session_id: watch_session_id.clone(),
workspace_id: workspace_id.to_string(),
debounce: WatchDebounceV1 {
quiet_period_milliseconds: 0,
maximum_delay_milliseconds: 0,
},
supersession_policy: "cancel_obsolete".into(),
event_detail: "summary".into(),
event_journal_capacity: 16,
})
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let manifest = WorkspaceManifestV1 {
schema: WORKSPACE_MANIFEST_V1_SCHEMA.into(),
version: 1,
workspace_id: workspace_id.to_string(),
packages: vec![WorkspacePackageDescriptorV1 {
package_id: "project".into(),
session_id: session_id.clone(),
display_name: Some("project".into()),
configuration_identity_hint: None,
metadata: BTreeMap::new(),
}],
dependencies: Vec::new(),
policy: WorkspacePolicyV1 {
failure_mode: "fail_fast".into(),
execution_mode: "deterministic_serial".into(),
result_detail: "summary".into(),
},
};
service
.submit_watch_change_batch(
WatchChangeBatchV1 {
schema: WATCH_CHANGE_BATCH_V1_SCHEMA.into(),
version: 1,
watch_session_id: watch_session_id.clone(),
sequence: 1,
observed_changes: sources
.iter()
.map(|source| ObservedChangeV1 {
kind: "modified".into(),
logical_path: source.logical_path.clone(),
previous_logical_path: None,
})
.collect(),
candidate: WatchCandidateV1 {
candidate_id: "initial-complete-candidate".into(),
fingerprint_hint: None,
verify_exact_equivalence: false,
workspace_request: WorkspaceCompileRequestV1 {
manifest,
packages: vec![WorkspacePackageCompileRequestV1 {
package_id: "project".into(),
expected_commit_sequence: 0,
request: CompileRequest {
configuration: envelope.configuration,
candidate_snapshot: snapshot,
sources: sources
.into_iter()
.map(|source| CompleteSource {
path: source.logical_path,
source: source.content,
language: None,
})
.collect(),
mode: RequestedCompilationMode::Automatic,
incremental_report: IncrementalReportSelector::Summary,
verify_exact_equivalence: false,
cache_report: CacheReportSelector::Summary,
},
}],
operation_id: "presolve-cli-watch-once-v1".into(),
},
},
},
0,
)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
let report = service
.run_watch_scheduler_turn(&watch_session_id, 0)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?
.ok_or(CliWorkspaceErrorV1 {
code: "L9F004_WATCH_SCHEDULER_DID_NOT_RUN",
message: "L8 did not schedule candidate".into(),
})?;
service
.stop_watch_session(&watch_session_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
service
.close_session(&session_id)
.map_err(|error| CliWorkspaceErrorV1 {
code: error.code,
message: error.message,
})?;
Ok(report.outcome)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
use super::*;
use crate::parse_explicit_source_spec_v1;
static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
#[test]
fn l9f_delegates_one_explicit_package_to_l7_and_verifies_the_result() {
let root = std::env::temp_dir().join(format!(
"presolve-l9f-{}",
NEXT_ROOT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(root.join("src")).unwrap();
let config = root.join("presolve.json");
fs::write(
&config,
include_bytes!("../fixtures/configuration/minimum-cli-v1.json"),
)
.unwrap();
fs::write(root.join("src/main.ts"), "export const value = 1;\n").unwrap();
let result = run_explicit_workspace_v1(
&config,
&[parse_explicit_source_spec_v1("src/main.ts=src/main.ts").unwrap()],
true,
)
.unwrap();
assert_eq!(result.status, "succeeded");
assert!(result.package_snapshot_id.unwrap().starts_with("snapshot:"));
fs::remove_dir_all(root).unwrap();
}
}