use kcode_k1_rust_cache::{CheckIdentity, LockGeneration, SourceGeneration, UserRustCache};
use kcode_k1_rust_package::{K1Dependency, LibraryFamily, LibraryId, SourcePackage};
use kcode_k1_rust_podman::{CommandDiagnostics, RustPodman, RustPodmanError, RustPodmanPaths};
use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome, PublishStatus};
use kcode_k1_rust_registry::LocalRegistry;
use kcode_k1_transaction_id::TxId;
use std::collections::BTreeSet;
use std::error::Error;
use std::ffi::OsString;
use std::fmt::{Debug, Display, Formatter};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
type CodingResult<T> = Result<T, RustCodingError>;
pub trait RustPublishAuthorization {
fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String>;
}
#[derive(Clone, Debug)]
pub struct RustCodingConfigValues {
pub schema_id: String,
pub toolchain_policy: String,
pub image: String,
pub rust_toolchain: String,
pub check_policy: String,
pub target_triple: String,
pub command_policy: String,
pub podman_program: OsString,
}
#[derive(Clone, Debug)]
pub struct RustCodingConfig {
values: RustCodingConfigValues,
}
impl RustCodingConfig {
pub fn new(values: RustCodingConfigValues) -> CodingResult<Self> {
let text = [
&values.schema_id,
&values.toolchain_policy,
&values.image,
&values.rust_toolchain,
&values.check_policy,
&values.target_triple,
&values.command_policy,
];
if text.iter().any(|value| value.is_empty()) || values.podman_program.is_empty() {
return Err(RustCodingError::State(
"Rust coding configuration values must be nonempty".into(),
));
}
Ok(Self { values })
}
}
#[derive(Debug)]
pub enum CheckOutcome {
Reused,
Checked { diagnostics: CommandDiagnostics },
}
pub struct VerifiedCheck {
identity: CheckIdentity,
}
#[derive(Debug)]
pub struct PublishResult {
pub check: CheckOutcome,
pub outcome: PublishOutcome,
}
#[derive(Debug)]
pub struct BinaryBuild {
pub path: PathBuf,
pub check: CheckOutcome,
pub diagnostics: CommandDiagnostics,
}
#[derive(Debug)]
pub enum RustCodingError {
State(String),
Authorization(String),
DependencyUnavailable {
family: LibraryFamily,
requirement: String,
},
PublishDenied,
StaleCheck,
Podman(RustPodmanError),
AfterPublish {
outcome: PublishOutcome,
cause: Box<RustCodingError>,
},
}
impl Display for RustCodingError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, formatter)
}
}
impl Error for RustCodingError {}
pub struct K1RustCoding {
config: RustCodingConfig,
projection: Arc<K1RustProjection>,
cache: UserRustCache,
registry: LocalRegistry,
podman: RustPodman,
}
impl K1RustCoding {
pub fn open(
cache_root: impl AsRef<Path>,
user: TxId,
config: RustCodingConfig,
projection: Arc<K1RustProjection>,
) -> CodingResult<Self> {
let values = &config.values;
let cache = state_result(UserRustCache::open(
cache_root,
user,
&values.schema_id,
&values.toolchain_policy,
))?;
let registry = state_result(LocalRegistry::open(cache.registry_root()))?;
let podman = RustPodman::new(values.podman_program.clone(), values.image.clone())
.map_err(RustCodingError::Podman)?;
let value = Self {
config,
projection,
cache,
registry,
podman,
};
value.ensure_config()?;
Ok(value)
}
pub fn cache_epoch(&self) -> u64 {
self.cache.epoch()
}
pub fn check(&mut self, package: &SourcePackage) -> CodingResult<CheckOutcome> {
let (source, dependencies) = self.prepare_check(package)?;
let family = package.id().family();
if self
.current_identity(family, source, &dependencies)?
.is_some()
{
return Ok(CheckOutcome::Reused);
}
let (_, diagnostics) = self.run_check(family, source, &dependencies)?;
Ok(CheckOutcome::Checked { diagnostics })
}
pub fn check_fresh(&mut self, package: &SourcePackage) -> CodingResult<VerifiedCheck> {
let (source, dependencies) = self.prepare_check(package)?;
let (identity, _) = self.run_check(package.id().family(), source, &dependencies)?;
Ok(VerifiedCheck { identity })
}
pub fn current_check(
&mut self,
package: &SourcePackage,
) -> CodingResult<Option<VerifiedCheck>> {
let (source, dependencies) = self.prepare_check(package)?;
Ok(self
.current_identity(package.id().family(), source, &dependencies)?
.map(|identity| VerifiedCheck { identity }))
}
pub fn publish(
&mut self,
package: &SourcePackage,
gate: &dyn RustPublishAuthorization,
) -> CodingResult<PublishResult> {
let check = self.check(package)?;
let outcome = self.publish_authorized(package, gate)?;
Ok(PublishResult { check, outcome })
}
pub fn publish_checked(
&mut self,
package: &SourcePackage,
verified: &VerifiedCheck,
gate: &dyn RustPublishAuthorization,
) -> CodingResult<PublishOutcome> {
let current = self.current_check(package)?;
if current.as_ref().map(|value| &value.identity) != Some(&verified.identity) {
return Err(RustCodingError::StaleCheck);
}
self.publish_authorized(package, gate)
}
pub fn build_binary(
&mut self,
package: &SourcePackage,
binary: &str,
) -> CodingResult<BinaryBuild> {
validate_binary(binary)?;
let check = self.check(package)?;
let id = package.id();
let authority = id.family().authority().to_string();
let version = id.version().to_string();
let directory = ensure_directories(
&self.cache.binaries_root(),
&[&authority, id.family().logical_name(), &version],
)?;
let path = directory.join(binary);
let diagnostics = self
.podman
.build_binary(&self.paths(id.family()), binary, &path)
.map_err(RustCodingError::Podman)?;
Ok(BinaryBuild {
path,
check,
diagnostics,
})
}
pub fn reset(&mut self) -> CodingResult<()> {
let values = &self.config.values;
state_result(
self.cache
.reset(&values.schema_id, &values.toolchain_policy),
)?;
self.registry = state_result(LocalRegistry::open(self.cache.registry_root()))?;
self.ensure_config()
}
fn prepare_check(
&mut self,
package: &SourcePackage,
) -> CodingResult<(SourceGeneration, Vec<LibraryId>)> {
let source = state_result(self.cache.materialize(package))?;
let dependencies = self.resolve_dependencies(package)?;
self.ensure_config()?;
Ok((source, dependencies))
}
fn current_identity(
&self,
family: &LibraryFamily,
source: SourceGeneration,
dependencies: &[LibraryId],
) -> CodingResult<Option<CheckIdentity>> {
let Some(lock) = state_result(self.cache.lock_generation(family))? else {
return Ok(None);
};
let identity = self.identity(source, lock, dependencies);
Ok(state_result(self.cache.has_check(family, &identity))?.then_some(identity))
}
fn run_check(
&mut self,
family: &LibraryFamily,
source: SourceGeneration,
dependencies: &[LibraryId],
) -> CodingResult<(CheckIdentity, CommandDiagnostics)> {
let paths = self.paths(family);
let diagnostics = self.podman.check(&paths).map_err(RustCodingError::Podman)?;
let lock = state_result(
self.cache
.record_lock(family, &read_ordinary(&paths.workspace.join("Cargo.lock"))?),
)?;
let identity = self.identity(source, lock, dependencies);
state_result(self.cache.record_check(family, &identity))?;
Ok((identity, diagnostics))
}
fn publish_authorized(
&self,
package: &SourcePackage,
gate: &dyn RustPublishAuthorization,
) -> CodingResult<PublishOutcome> {
match gate.authorize_publish(package.id().family()) {
Ok(true) => {}
Ok(false) => return Err(RustCodingError::PublishDenied),
Err(cause) => return Err(RustCodingError::Authorization(cause)),
}
let outcome = state_result(self.projection.publish(package))?;
if outcome.status() != PublishStatus::Conflict
&& let Err(cause) = self.complete_publication(package)
{
return Err(RustCodingError::AfterPublish {
outcome,
cause: Box::new(cause),
});
}
Ok(outcome)
}
fn resolve_dependencies(&mut self, package: &SourcePackage) -> CodingResult<Vec<LibraryId>> {
let mut visiting = BTreeSet::new();
let mut resolved = BTreeSet::new();
for dependency in package.dependencies() {
self.resolve(dependency, &mut visiting, &mut resolved)?;
}
Ok(resolved.into_iter().collect())
}
fn resolve(
&mut self,
dependency: &K1Dependency,
visiting: &mut BTreeSet<LibraryId>,
resolved: &mut BTreeSet<LibraryId>,
) -> CodingResult<()> {
let package = state_result(
self.projection
.resolve(dependency.family(), dependency.requirement()),
)?
.ok_or_else(|| RustCodingError::DependencyUnavailable {
family: dependency.family().clone(),
requirement: dependency.requirement().to_string(),
})?;
let id = package.id().clone();
if resolved.contains(&id) || !visiting.insert(id.clone()) {
return Ok(());
}
let result = self.resolve_one(&package, visiting, resolved);
visiting.remove(&id);
if result.is_ok() {
resolved.insert(id);
}
result
}
fn resolve_one(
&mut self,
package: &SourcePackage,
visiting: &mut BTreeSet<LibraryId>,
resolved: &mut BTreeSet<LibraryId>,
) -> CodingResult<()> {
for dependency in package.dependencies() {
self.resolve(dependency, visiting, resolved)?;
}
self.install(package)
}
fn install(&self, package: &SourcePackage) -> CodingResult<()> {
state_result(self.registry.install(package)).map(|_| ())
}
fn complete_publication(&self, package: &SourcePackage) -> CodingResult<()> {
self.install(package)?;
self.ensure_config()
}
fn identity(
&self,
source: SourceGeneration,
lock: LockGeneration,
dependencies: &[LibraryId],
) -> CheckIdentity {
let values = &self.config.values;
CheckIdentity {
source,
lock,
dependencies: dependencies.to_vec(),
image_identity: values.image.clone(),
rust_toolchain: values.rust_toolchain.clone(),
check_policy: values.check_policy.clone(),
target_triple: values.target_triple.clone(),
command_policy: values.command_policy.clone(),
}
}
fn paths(&self, family: &LibraryFamily) -> RustPodmanPaths {
RustPodmanPaths::new(
self.cache.workspace(family),
self.cache.cargo_home(),
self.cache.target_root(),
self.cache.registry_root(),
self.cache.cargo_home().join("config.toml"),
)
}
fn ensure_config(&self) -> CodingResult<()> {
write_config(
&self.cache.cargo_home().join("config.toml"),
self.registry.cargo_config().as_bytes(),
)
}
}
fn read_ordinary(path: &Path) -> CodingResult<Vec<u8>> {
let metadata = fs::symlink_metadata(path).map_err(state)?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(RustCodingError::State(format!(
"not an ordinary nonsymlink file: {}",
path.display()
)));
}
fs::read(path).map_err(state)
}
fn write_config(path: &Path, bytes: &[u8]) -> CodingResult<()> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(RustCodingError::State(format!(
"Cargo configuration is not an ordinary nonsymlink file: {}",
path.display()
)));
}
Ok(_) if fs::read(path).map_err(state)? == bytes => return Ok(()),
Ok(_) => {}
Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
Err(cause) => return Err(state(cause)),
}
let parent = path
.parent()
.ok_or_else(|| RustCodingError::State("Cargo configuration has no parent".into()))?;
let stage = parent.join(format!(".config.toml.stage-{}", std::process::id()));
match fs::symlink_metadata(&stage) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(RustCodingError::State(format!(
"Cargo configuration stage is not an ordinary nonsymlink file: {}",
stage.display()
)));
}
Ok(_) => fs::remove_file(&stage).map_err(state)?,
Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
Err(cause) => return Err(state(cause)),
}
let result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&stage)?;
file.write_all(bytes)?;
file.sync_all()?;
fs::rename(&stage, path)
})()
.map_err(state);
if result.is_err() {
let _ = fs::remove_file(stage);
}
result
}
fn ensure_directories(root: &Path, names: &[&str]) -> CodingResult<PathBuf> {
let mut path = root.to_path_buf();
require_directory(&path)?;
for name in names {
path.push(name);
match fs::create_dir(&path) {
Ok(()) => {}
Err(cause) if cause.kind() == std::io::ErrorKind::AlreadyExists => {
require_directory(&path)?;
}
Err(cause) => return Err(state(cause)),
}
}
Ok(path)
}
fn require_directory(path: &Path) -> CodingResult<()> {
let metadata = fs::symlink_metadata(path).map_err(state)?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(RustCodingError::State(format!(
"not an ordinary nonsymlink directory: {}",
path.display()
)));
}
Ok(())
}
fn validate_binary(value: &str) -> CodingResult<()> {
let bytes = value.as_bytes();
let alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
let valid = (1..=250).contains(&bytes.len())
&& alphanumeric(&bytes[0])
&& alphanumeric(&bytes[bytes.len() - 1])
&& bytes.iter().all(|byte| alphanumeric(byte) || *byte == b'-')
&& !bytes.windows(2).any(|pair| pair == b"--");
if valid {
Ok(())
} else {
Err(RustCodingError::Podman(RustPodmanError::InvalidInput {
field: "binary",
reason: "must be lowercase kebab-case of length 1-250".into(),
}))
}
}
fn state(cause: impl Display) -> RustCodingError {
RustCodingError::State(cause.to_string())
}
fn state_result<T>(result: Result<T, String>) -> CodingResult<T> {
result.map_err(RustCodingError::State)
}