#[cfg(any(feature = "norn", feature = "acp"))]
use std::path::PathBuf;
#[cfg(feature = "acp")]
use std::time::Duration;
#[cfg(any(feature = "norn", feature = "acp"))]
use aion_awl::CwdSource;
use aion_awl::{CompiledHarness, CompiledHarnessKind};
#[cfg(any(feature = "norn", feature = "acp"))]
use aion_integrations::HarnessWorkspace;
use aion_integrations::{ChildEnvironment, EnvironmentDeclaration};
use anyhow::{Context, Result};
use super::catalogue::{self, HarnessKind};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum HarnessConfig {
#[cfg(feature = "norn")]
Norn(NornConfig),
#[cfg(feature = "acp")]
Acp(AcpConfig),
}
#[cfg(feature = "norn")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct NornConfig {
pub(crate) binary: Option<PathBuf>,
pub(crate) workspace: HarnessWorkspace,
pub(crate) args: Vec<String>,
pub(crate) environment: ChildEnvironment,
}
#[cfg(feature = "acp")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct AcpConfig {
pub(crate) command: PathBuf,
pub(crate) args: Vec<String>,
pub(crate) workspace: HarnessWorkspace,
pub(crate) permission: aion_integration_acp::PermissionPolicy,
pub(crate) environment: ChildEnvironment,
pub(crate) exit_grace: Duration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ResolvedHarness {
pub(crate) kind: HarnessKind,
pub(crate) config: HarnessConfig,
}
impl ResolvedHarness {
#[cfg(any(feature = "norn", feature = "acp"))]
pub(crate) const fn environment(&self) -> &ChildEnvironment {
match &self.config {
#[cfg(feature = "norn")]
HarnessConfig::Norn(norn) => &norn.environment,
#[cfg(feature = "acp")]
HarnessConfig::Acp(acp) => &acp.environment,
}
}
#[cfg(any(feature = "norn", feature = "acp"))]
pub(crate) const fn launch_workspace(&self) -> &HarnessWorkspace {
match &self.config {
#[cfg(feature = "norn")]
HarnessConfig::Norn(norn) => &norn.workspace,
#[cfg(feature = "acp")]
HarnessConfig::Acp(acp) => &acp.workspace,
}
}
#[cfg(any(feature = "norn", feature = "acp"))]
pub(crate) fn launch_directory_report(&self) -> String {
match self.launch_workspace() {
HarnessWorkspace::Fixed(directory) => directory.display().to_string(),
HarnessWorkspace::PerRun(parameter) => {
format!("each job names its own directory in its `{parameter}` parameter")
}
}
}
}
pub(crate) fn resolve(
declared: &CompiledHarness,
document: &std::path::Path,
) -> Result<Option<ResolvedHarness>> {
resolve_against(
declared,
document,
&aion_server::worker::WorkspaceRoot::resolve(),
)
}
pub(crate) fn resolve_against(
declared: &CompiledHarness,
document: &std::path::Path,
workspace_root: &aion_server::worker::WorkspaceRoot,
) -> Result<Option<ResolvedHarness>> {
let Some(kind) = catalogue::resolve(declared.kind.word(), document)? else {
return Ok(None);
};
let environment = child_environment(declared, document)?;
let config = build(kind, declared, environment, workspace_root, document)?;
Ok(Some(ResolvedHarness { kind, config }))
}
fn child_environment(
declared: &CompiledHarness,
document: &std::path::Path,
) -> Result<ChildEnvironment> {
let declaration =
EnvironmentDeclaration::new(declared.env_pass.clone()).with_context(|| {
format!(
"the `harness` section of {} declares an unusable `env_pass` list",
document.display()
)
})?;
Ok(declaration.resolve_from_process())
}
fn build(
kind: HarnessKind,
declared: &CompiledHarness,
environment: ChildEnvironment,
workspace_root: &aion_server::worker::WorkspaceRoot,
document: &std::path::Path,
) -> Result<HarnessConfig> {
match (kind, &declared.kind) {
#[cfg(feature = "norn")]
(HarnessKind::Norn, CompiledHarnessKind::Norn { binary, cwd }) => {
Ok(HarnessConfig::Norn(NornConfig {
binary: binary
.as_ref()
.map(|binary| {
launch_path(
aion_awl::HARNESS_BINARY_FIELD,
binary,
workspace_root,
document,
)
})
.transpose()?,
workspace: launch_workspace(
aion_awl::HARNESS_CWD_FIELD,
cwd,
workspace_root,
document,
)?,
args: declared.args.clone(),
environment,
}))
}
#[cfg(feature = "acp")]
(
HarnessKind::Acp,
CompiledHarnessKind::Acp {
command,
cwd,
permission,
exit_grace,
},
) => Ok(HarnessConfig::Acp(AcpConfig {
command: launch_path(
aion_awl::HARNESS_COMMAND_FIELD,
command,
workspace_root,
document,
)?,
args: declared.args.clone(),
workspace: launch_workspace(
aion_awl::HARNESS_CWD_FIELD,
cwd,
workspace_root,
document,
)?,
permission: permission_policy(*permission),
environment,
exit_grace: *exit_grace,
})),
(resolved, declared_kind) => anyhow::bail!(
"the document declares the `{}` harness, passing {} environment variable(s), but \
this build resolved `{}`; the two are chosen from one table and cannot disagree, \
so this is a composition-root defect",
declared_kind.word(),
environment.declared().len(),
resolved.name()
),
}
}
#[cfg(any(feature = "norn", feature = "acp"))]
fn launch_workspace(
field: &str,
declared: &CwdSource,
workspace_root: &aion_server::worker::WorkspaceRoot,
document: &std::path::Path,
) -> Result<HarnessWorkspace> {
match declared {
CwdSource::Literal(path) => {
launch_path(field, path, workspace_root, document).map(HarnessWorkspace::Fixed)
}
CwdSource::Bound(parameter) => Ok(HarnessWorkspace::PerRun(parameter.clone())),
}
}
#[cfg(any(feature = "norn", feature = "acp"))]
fn launch_path(
field: &str,
declared: &std::path::Path,
workspace_root: &aion_server::worker::WorkspaceRoot,
document: &std::path::Path,
) -> Result<PathBuf> {
let written = declared.to_str().ok_or_else(|| {
anyhow::anyhow!(
"the `harness` section of {} sets `{field}` to a path with no UTF-8 spelling, so it \
cannot be checked for the `{placeholder}` the document may carry",
document.display(),
placeholder = aion_server::worker::WORKSPACE_ROOT_PLACEHOLDER,
)
})?;
let expanded = workspace_root.expand_setting(written).with_context(|| {
format!(
"the `harness` section of {} sets `{field}` to `{written}`, which asks this box for \
its workspace root",
document.display()
)
})?;
let resolved = match expanded {
Some(expanded) => PathBuf::from(expanded),
None => declared.to_path_buf(),
};
anyhow::ensure!(
resolved.is_absolute(),
"the `harness` section of {} sets `{field}` to `{written}`, which this box expands to \
`{}` — and that is not an absolute path. A worker is started from whatever directory a \
supervisor or a shell happens to be in, so a relative path names a different place on \
every launch; the launch is refused here rather than rooted somewhere nobody chose.",
document.display(),
resolved.display()
);
Ok(resolved)
}
#[cfg(feature = "acp")]
const fn permission_policy(
declared: aion_awl::HarnessPermission,
) -> aion_integration_acp::PermissionPolicy {
match declared {
aion_awl::HarnessPermission::Deny => aion_integration_acp::PermissionPolicy::Deny,
aion_awl::HarnessPermission::AllowOnce => aion_integration_acp::PermissionPolicy::AllowOnce,
}
}
#[cfg(test)]
pub(crate) fn test_environment() -> ChildEnvironment {
EnvironmentDeclaration::empty().resolve(&std::collections::BTreeMap::new())
}