Skip to main content

kcode_k1_rust_coding/
lib.rs

1use kcode_k1_rust_cache::{CheckIdentity, LockGeneration, SourceGeneration, UserRustCache};
2use kcode_k1_rust_package::{K1Dependency, LibraryFamily, LibraryId, SourcePackage};
3use kcode_k1_rust_podman::{CommandDiagnostics, RustPodman, RustPodmanError, RustPodmanPaths};
4use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome, PublishStatus};
5use kcode_k1_rust_registry::LocalRegistry;
6use kcode_k1_transaction_id::TxId;
7use std::collections::BTreeSet;
8use std::error::Error;
9use std::ffi::OsString;
10use std::fmt::{Debug, Display, Formatter};
11use std::fs::{self, OpenOptions};
12use std::io::Write;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16type CodingResult<T> = Result<T, RustCodingError>;
17
18pub trait RustPublishAuthorization {
19    fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String>;
20}
21
22#[derive(Clone, Debug)]
23pub struct RustCodingConfigValues {
24    pub schema_id: String,
25    pub toolchain_policy: String,
26    pub image: String,
27    pub rust_toolchain: String,
28    pub check_policy: String,
29    pub target_triple: String,
30    pub command_policy: String,
31    pub podman_program: OsString,
32}
33
34#[derive(Clone, Debug)]
35pub struct RustCodingConfig {
36    values: RustCodingConfigValues,
37}
38
39impl RustCodingConfig {
40    pub fn new(values: RustCodingConfigValues) -> CodingResult<Self> {
41        let text = [
42            &values.schema_id,
43            &values.toolchain_policy,
44            &values.image,
45            &values.rust_toolchain,
46            &values.check_policy,
47            &values.target_triple,
48            &values.command_policy,
49        ];
50        if text.iter().any(|value| value.is_empty()) || values.podman_program.is_empty() {
51            return Err(RustCodingError::State(
52                "Rust coding configuration values must be nonempty".into(),
53            ));
54        }
55        Ok(Self { values })
56    }
57}
58
59#[derive(Debug)]
60pub enum CheckOutcome {
61    Reused,
62    Checked { diagnostics: CommandDiagnostics },
63}
64
65pub struct VerifiedCheck {
66    identity: CheckIdentity,
67}
68
69#[derive(Debug)]
70pub struct PublishResult {
71    pub check: CheckOutcome,
72    pub outcome: PublishOutcome,
73}
74
75#[derive(Debug)]
76pub struct BinaryBuild {
77    pub path: PathBuf,
78    pub check: CheckOutcome,
79    pub diagnostics: CommandDiagnostics,
80}
81
82#[derive(Debug)]
83pub enum RustCodingError {
84    State(String),
85    Authorization(String),
86    DependencyUnavailable {
87        family: LibraryFamily,
88        requirement: String,
89    },
90    PublishDenied,
91    StaleCheck,
92    Podman(RustPodmanError),
93    AfterPublish {
94        outcome: PublishOutcome,
95        cause: Box<RustCodingError>,
96    },
97}
98
99impl Display for RustCodingError {
100    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
101        Debug::fmt(self, formatter)
102    }
103}
104
105impl Error for RustCodingError {}
106
107pub struct K1RustCoding {
108    config: RustCodingConfig,
109    projection: Arc<K1RustProjection>,
110    cache: UserRustCache,
111    registry: LocalRegistry,
112    podman: RustPodman,
113}
114
115impl K1RustCoding {
116    pub fn open(
117        cache_root: impl AsRef<Path>,
118        user: TxId,
119        config: RustCodingConfig,
120        projection: Arc<K1RustProjection>,
121    ) -> CodingResult<Self> {
122        let values = &config.values;
123        let cache = state_result(UserRustCache::open(
124            cache_root,
125            user,
126            &values.schema_id,
127            &values.toolchain_policy,
128        ))?;
129        let registry = state_result(LocalRegistry::open(cache.registry_root()))?;
130        let podman = RustPodman::new(values.podman_program.clone(), values.image.clone())
131            .map_err(RustCodingError::Podman)?;
132        let value = Self {
133            config,
134            projection,
135            cache,
136            registry,
137            podman,
138        };
139        value.ensure_config()?;
140        Ok(value)
141    }
142
143    pub fn cache_epoch(&self) -> u64 {
144        self.cache.epoch()
145    }
146
147    pub fn check(&mut self, package: &SourcePackage) -> CodingResult<CheckOutcome> {
148        let (source, dependencies) = self.prepare_check(package)?;
149        let family = package.id().family();
150        if self
151            .current_identity(family, source, &dependencies)?
152            .is_some()
153        {
154            return Ok(CheckOutcome::Reused);
155        }
156        let (_, diagnostics) = self.run_check(family, source, &dependencies)?;
157        Ok(CheckOutcome::Checked { diagnostics })
158    }
159
160    pub fn check_fresh(&mut self, package: &SourcePackage) -> CodingResult<VerifiedCheck> {
161        let (source, dependencies) = self.prepare_check(package)?;
162        let (identity, _) = self.run_check(package.id().family(), source, &dependencies)?;
163        Ok(VerifiedCheck { identity })
164    }
165
166    pub fn current_check(
167        &mut self,
168        package: &SourcePackage,
169    ) -> CodingResult<Option<VerifiedCheck>> {
170        let (source, dependencies) = self.prepare_check(package)?;
171        Ok(self
172            .current_identity(package.id().family(), source, &dependencies)?
173            .map(|identity| VerifiedCheck { identity }))
174    }
175
176    pub fn publish(
177        &mut self,
178        package: &SourcePackage,
179        gate: &dyn RustPublishAuthorization,
180    ) -> CodingResult<PublishResult> {
181        let check = self.check(package)?;
182        let outcome = self.publish_authorized(package, gate)?;
183        Ok(PublishResult { check, outcome })
184    }
185
186    pub fn publish_checked(
187        &mut self,
188        package: &SourcePackage,
189        verified: &VerifiedCheck,
190        gate: &dyn RustPublishAuthorization,
191    ) -> CodingResult<PublishOutcome> {
192        let current = self.current_check(package)?;
193        if current.as_ref().map(|value| &value.identity) != Some(&verified.identity) {
194            return Err(RustCodingError::StaleCheck);
195        }
196        self.publish_authorized(package, gate)
197    }
198
199    pub fn build_binary(
200        &mut self,
201        package: &SourcePackage,
202        binary: &str,
203    ) -> CodingResult<BinaryBuild> {
204        validate_binary(binary)?;
205        let check = self.check(package)?;
206        let id = package.id();
207        let authority = id.family().authority().to_string();
208        let version = id.version().to_string();
209        let directory = ensure_directories(
210            &self.cache.binaries_root(),
211            &[&authority, id.family().logical_name(), &version],
212        )?;
213        let path = directory.join(binary);
214        let diagnostics = self
215            .podman
216            .build_binary(&self.paths(id.family()), binary, &path)
217            .map_err(RustCodingError::Podman)?;
218        Ok(BinaryBuild {
219            path,
220            check,
221            diagnostics,
222        })
223    }
224
225    pub fn reset(&mut self) -> CodingResult<()> {
226        let values = &self.config.values;
227        state_result(
228            self.cache
229                .reset(&values.schema_id, &values.toolchain_policy),
230        )?;
231        self.registry = state_result(LocalRegistry::open(self.cache.registry_root()))?;
232        self.ensure_config()
233    }
234
235    fn prepare_check(
236        &mut self,
237        package: &SourcePackage,
238    ) -> CodingResult<(SourceGeneration, Vec<LibraryId>)> {
239        let source = state_result(self.cache.materialize(package))?;
240        let dependencies = self.resolve_dependencies(package)?;
241        self.ensure_config()?;
242        Ok((source, dependencies))
243    }
244
245    fn current_identity(
246        &self,
247        family: &LibraryFamily,
248        source: SourceGeneration,
249        dependencies: &[LibraryId],
250    ) -> CodingResult<Option<CheckIdentity>> {
251        let Some(lock) = state_result(self.cache.lock_generation(family))? else {
252            return Ok(None);
253        };
254        let identity = self.identity(source, lock, dependencies);
255        Ok(state_result(self.cache.has_check(family, &identity))?.then_some(identity))
256    }
257
258    fn run_check(
259        &mut self,
260        family: &LibraryFamily,
261        source: SourceGeneration,
262        dependencies: &[LibraryId],
263    ) -> CodingResult<(CheckIdentity, CommandDiagnostics)> {
264        let paths = self.paths(family);
265        let diagnostics = self.podman.check(&paths).map_err(RustCodingError::Podman)?;
266        let lock = state_result(
267            self.cache
268                .record_lock(family, &read_ordinary(&paths.workspace.join("Cargo.lock"))?),
269        )?;
270        let identity = self.identity(source, lock, dependencies);
271        state_result(self.cache.record_check(family, &identity))?;
272        Ok((identity, diagnostics))
273    }
274
275    fn publish_authorized(
276        &self,
277        package: &SourcePackage,
278        gate: &dyn RustPublishAuthorization,
279    ) -> CodingResult<PublishOutcome> {
280        match gate.authorize_publish(package.id().family()) {
281            Ok(true) => {}
282            Ok(false) => return Err(RustCodingError::PublishDenied),
283            Err(cause) => return Err(RustCodingError::Authorization(cause)),
284        }
285        let outcome = state_result(self.projection.publish(package))?;
286        if outcome.status() != PublishStatus::Conflict
287            && let Err(cause) = self.complete_publication(package)
288        {
289            return Err(RustCodingError::AfterPublish {
290                outcome,
291                cause: Box::new(cause),
292            });
293        }
294        Ok(outcome)
295    }
296
297    fn resolve_dependencies(&mut self, package: &SourcePackage) -> CodingResult<Vec<LibraryId>> {
298        let mut visiting = BTreeSet::new();
299        let mut resolved = BTreeSet::new();
300        for dependency in package.dependencies() {
301            self.resolve(dependency, &mut visiting, &mut resolved)?;
302        }
303        Ok(resolved.into_iter().collect())
304    }
305
306    fn resolve(
307        &mut self,
308        dependency: &K1Dependency,
309        visiting: &mut BTreeSet<LibraryId>,
310        resolved: &mut BTreeSet<LibraryId>,
311    ) -> CodingResult<()> {
312        let package = state_result(
313            self.projection
314                .resolve(dependency.family(), dependency.requirement()),
315        )?
316        .ok_or_else(|| RustCodingError::DependencyUnavailable {
317            family: dependency.family().clone(),
318            requirement: dependency.requirement().to_string(),
319        })?;
320        let id = package.id().clone();
321        if resolved.contains(&id) || !visiting.insert(id.clone()) {
322            return Ok(());
323        }
324        let result = self.resolve_one(&package, visiting, resolved);
325        visiting.remove(&id);
326        if result.is_ok() {
327            resolved.insert(id);
328        }
329        result
330    }
331
332    fn resolve_one(
333        &mut self,
334        package: &SourcePackage,
335        visiting: &mut BTreeSet<LibraryId>,
336        resolved: &mut BTreeSet<LibraryId>,
337    ) -> CodingResult<()> {
338        for dependency in package.dependencies() {
339            self.resolve(dependency, visiting, resolved)?;
340        }
341        self.install(package)
342    }
343
344    fn install(&self, package: &SourcePackage) -> CodingResult<()> {
345        state_result(self.registry.install(package)).map(|_| ())
346    }
347
348    fn complete_publication(&self, package: &SourcePackage) -> CodingResult<()> {
349        self.install(package)?;
350        self.ensure_config()
351    }
352
353    fn identity(
354        &self,
355        source: SourceGeneration,
356        lock: LockGeneration,
357        dependencies: &[LibraryId],
358    ) -> CheckIdentity {
359        let values = &self.config.values;
360        CheckIdentity {
361            source,
362            lock,
363            dependencies: dependencies.to_vec(),
364            image_identity: values.image.clone(),
365            rust_toolchain: values.rust_toolchain.clone(),
366            check_policy: values.check_policy.clone(),
367            target_triple: values.target_triple.clone(),
368            command_policy: values.command_policy.clone(),
369        }
370    }
371
372    fn paths(&self, family: &LibraryFamily) -> RustPodmanPaths {
373        RustPodmanPaths::new(
374            self.cache.workspace(family),
375            self.cache.cargo_home(),
376            self.cache.target_root(),
377            self.cache.registry_root(),
378            self.cache.cargo_home().join("config.toml"),
379        )
380    }
381
382    fn ensure_config(&self) -> CodingResult<()> {
383        write_config(
384            &self.cache.cargo_home().join("config.toml"),
385            self.registry.cargo_config().as_bytes(),
386        )
387    }
388}
389
390fn read_ordinary(path: &Path) -> CodingResult<Vec<u8>> {
391    let metadata = fs::symlink_metadata(path).map_err(state)?;
392    if metadata.file_type().is_symlink() || !metadata.is_file() {
393        return Err(RustCodingError::State(format!(
394            "not an ordinary nonsymlink file: {}",
395            path.display()
396        )));
397    }
398    fs::read(path).map_err(state)
399}
400
401fn write_config(path: &Path, bytes: &[u8]) -> CodingResult<()> {
402    match fs::symlink_metadata(path) {
403        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
404            return Err(RustCodingError::State(format!(
405                "Cargo configuration is not an ordinary nonsymlink file: {}",
406                path.display()
407            )));
408        }
409        Ok(_) if fs::read(path).map_err(state)? == bytes => return Ok(()),
410        Ok(_) => {}
411        Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
412        Err(cause) => return Err(state(cause)),
413    }
414    let parent = path
415        .parent()
416        .ok_or_else(|| RustCodingError::State("Cargo configuration has no parent".into()))?;
417    let stage = parent.join(format!(".config.toml.stage-{}", std::process::id()));
418    match fs::symlink_metadata(&stage) {
419        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
420            return Err(RustCodingError::State(format!(
421                "Cargo configuration stage is not an ordinary nonsymlink file: {}",
422                stage.display()
423            )));
424        }
425        Ok(_) => fs::remove_file(&stage).map_err(state)?,
426        Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
427        Err(cause) => return Err(state(cause)),
428    }
429    let result = (|| {
430        let mut file = OpenOptions::new()
431            .write(true)
432            .create_new(true)
433            .open(&stage)?;
434        file.write_all(bytes)?;
435        file.sync_all()?;
436        fs::rename(&stage, path)
437    })()
438    .map_err(state);
439    if result.is_err() {
440        let _ = fs::remove_file(stage);
441    }
442    result
443}
444
445fn ensure_directories(root: &Path, names: &[&str]) -> CodingResult<PathBuf> {
446    let mut path = root.to_path_buf();
447    require_directory(&path)?;
448    for name in names {
449        path.push(name);
450        match fs::create_dir(&path) {
451            Ok(()) => {}
452            Err(cause) if cause.kind() == std::io::ErrorKind::AlreadyExists => {
453                require_directory(&path)?;
454            }
455            Err(cause) => return Err(state(cause)),
456        }
457    }
458    Ok(path)
459}
460
461fn require_directory(path: &Path) -> CodingResult<()> {
462    let metadata = fs::symlink_metadata(path).map_err(state)?;
463    if metadata.file_type().is_symlink() || !metadata.is_dir() {
464        return Err(RustCodingError::State(format!(
465            "not an ordinary nonsymlink directory: {}",
466            path.display()
467        )));
468    }
469    Ok(())
470}
471
472fn validate_binary(value: &str) -> CodingResult<()> {
473    let bytes = value.as_bytes();
474    let alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
475    let valid = (1..=250).contains(&bytes.len())
476        && alphanumeric(&bytes[0])
477        && alphanumeric(&bytes[bytes.len() - 1])
478        && bytes.iter().all(|byte| alphanumeric(byte) || *byte == b'-')
479        && !bytes.windows(2).any(|pair| pair == b"--");
480    if valid {
481        Ok(())
482    } else {
483        Err(RustCodingError::Podman(RustPodmanError::InvalidInput {
484            field: "binary",
485            reason: "must be lowercase kebab-case of length 1-250".into(),
486        }))
487    }
488}
489
490fn state(cause: impl Display) -> RustCodingError {
491    RustCodingError::State(cause.to_string())
492}
493
494fn state_result<T>(result: Result<T, String>) -> CodingResult<T> {
495    result.map_err(RustCodingError::State)
496}