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
65#[derive(Debug)]
66pub struct PublishResult {
67    pub check: CheckOutcome,
68    pub outcome: PublishOutcome,
69}
70
71#[derive(Debug)]
72pub struct BinaryBuild {
73    pub path: PathBuf,
74    pub check: CheckOutcome,
75    pub diagnostics: CommandDiagnostics,
76}
77
78#[derive(Debug)]
79pub enum RustCodingError {
80    State(String),
81    Authorization(String),
82    DependencyUnavailable {
83        family: LibraryFamily,
84        requirement: String,
85    },
86    PublishDenied,
87    Podman(RustPodmanError),
88    AfterPublish {
89        outcome: PublishOutcome,
90        cause: Box<RustCodingError>,
91    },
92}
93
94impl Display for RustCodingError {
95    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
96        Debug::fmt(self, formatter)
97    }
98}
99
100impl Error for RustCodingError {}
101
102pub struct K1RustCoding {
103    config: RustCodingConfig,
104    projection: Arc<K1RustProjection>,
105    cache: UserRustCache,
106    registry: LocalRegistry,
107    podman: RustPodman,
108}
109
110impl K1RustCoding {
111    pub fn open(
112        cache_root: impl AsRef<Path>,
113        user: TxId,
114        config: RustCodingConfig,
115        projection: Arc<K1RustProjection>,
116    ) -> CodingResult<Self> {
117        let values = &config.values;
118        let cache = state_result(UserRustCache::open(
119            cache_root,
120            user,
121            &values.schema_id,
122            &values.toolchain_policy,
123        ))?;
124        let registry = state_result(LocalRegistry::open(cache.registry_root()))?;
125        let podman = RustPodman::new(values.podman_program.clone(), values.image.clone())
126            .map_err(RustCodingError::Podman)?;
127        let value = Self {
128            config,
129            projection,
130            cache,
131            registry,
132            podman,
133        };
134        value.ensure_config()?;
135        Ok(value)
136    }
137
138    pub fn cache_epoch(&self) -> u64 {
139        self.cache.epoch()
140    }
141
142    pub fn check(&mut self, package: &SourcePackage) -> CodingResult<CheckOutcome> {
143        let source = state_result(self.cache.materialize(package))?;
144        let dependencies = self.resolve_dependencies(package)?;
145        self.ensure_config()?;
146        let family = package.id().family();
147        if let Some(lock) = state_result(self.cache.lock_generation(family))? {
148            let identity = self.identity(source, lock, &dependencies);
149            if state_result(self.cache.has_check(family, &identity))? {
150                return Ok(CheckOutcome::Reused);
151            }
152        }
153        let paths = self.paths(family);
154        let diagnostics = self.podman.check(&paths).map_err(RustCodingError::Podman)?;
155        let lock = state_result(
156            self.cache
157                .record_lock(family, &read_ordinary(&paths.workspace.join("Cargo.lock"))?),
158        )?;
159        let identity = self.identity(source, lock, &dependencies);
160        state_result(self.cache.record_check(family, &identity))?;
161        Ok(CheckOutcome::Checked { diagnostics })
162    }
163
164    pub fn publish(
165        &mut self,
166        package: &SourcePackage,
167        gate: &dyn RustPublishAuthorization,
168    ) -> CodingResult<PublishResult> {
169        let check = self.check(package)?;
170        match gate.authorize_publish(package.id().family()) {
171            Ok(true) => {}
172            Ok(false) => return Err(RustCodingError::PublishDenied),
173            Err(cause) => return Err(RustCodingError::Authorization(cause)),
174        }
175        let outcome = state_result(self.projection.publish(package))?;
176        if outcome.status() != PublishStatus::Conflict
177            && let Err(cause) = self.complete_publication(package)
178        {
179            return Err(RustCodingError::AfterPublish {
180                outcome,
181                cause: Box::new(cause),
182            });
183        }
184        Ok(PublishResult { check, outcome })
185    }
186
187    pub fn build_binary(
188        &mut self,
189        package: &SourcePackage,
190        binary: &str,
191    ) -> CodingResult<BinaryBuild> {
192        validate_binary(binary)?;
193        let check = self.check(package)?;
194        let id = package.id();
195        let authority = id.family().authority().to_string();
196        let version = id.version().to_string();
197        let directory = ensure_directories(
198            &self.cache.binaries_root(),
199            &[&authority, id.family().logical_name(), &version],
200        )?;
201        let path = directory.join(binary);
202        let diagnostics = self
203            .podman
204            .build_binary(&self.paths(id.family()), binary, &path)
205            .map_err(RustCodingError::Podman)?;
206        Ok(BinaryBuild {
207            path,
208            check,
209            diagnostics,
210        })
211    }
212
213    pub fn reset(&mut self) -> CodingResult<()> {
214        let values = &self.config.values;
215        state_result(
216            self.cache
217                .reset(&values.schema_id, &values.toolchain_policy),
218        )?;
219        self.registry = state_result(LocalRegistry::open(self.cache.registry_root()))?;
220        self.ensure_config()
221    }
222
223    fn resolve_dependencies(&mut self, package: &SourcePackage) -> CodingResult<Vec<LibraryId>> {
224        let mut visiting = BTreeSet::new();
225        let mut resolved = BTreeSet::new();
226        for dependency in package.dependencies() {
227            self.resolve(dependency, &mut visiting, &mut resolved)?;
228        }
229        Ok(resolved.into_iter().collect())
230    }
231
232    fn resolve(
233        &mut self,
234        dependency: &K1Dependency,
235        visiting: &mut BTreeSet<LibraryId>,
236        resolved: &mut BTreeSet<LibraryId>,
237    ) -> CodingResult<()> {
238        let package = state_result(
239            self.projection
240                .resolve(dependency.family(), dependency.requirement()),
241        )?
242        .ok_or_else(|| RustCodingError::DependencyUnavailable {
243            family: dependency.family().clone(),
244            requirement: dependency.requirement().to_string(),
245        })?;
246        let id = package.id().clone();
247        if resolved.contains(&id) || !visiting.insert(id.clone()) {
248            return Ok(());
249        }
250        let result = self.resolve_one(&package, visiting, resolved);
251        visiting.remove(&id);
252        if result.is_ok() {
253            resolved.insert(id);
254        }
255        result
256    }
257
258    fn resolve_one(
259        &mut self,
260        package: &SourcePackage,
261        visiting: &mut BTreeSet<LibraryId>,
262        resolved: &mut BTreeSet<LibraryId>,
263    ) -> CodingResult<()> {
264        for dependency in package.dependencies() {
265            self.resolve(dependency, visiting, resolved)?;
266        }
267        self.install(package)
268    }
269
270    fn install(&self, package: &SourcePackage) -> CodingResult<()> {
271        state_result(self.registry.install(package)).map(|_| ())
272    }
273
274    fn complete_publication(&self, package: &SourcePackage) -> CodingResult<()> {
275        self.install(package)?;
276        self.ensure_config()
277    }
278
279    fn identity(
280        &self,
281        source: SourceGeneration,
282        lock: LockGeneration,
283        dependencies: &[LibraryId],
284    ) -> CheckIdentity {
285        let values = &self.config.values;
286        CheckIdentity {
287            source,
288            lock,
289            dependencies: dependencies.to_vec(),
290            image_identity: values.image.clone(),
291            rust_toolchain: values.rust_toolchain.clone(),
292            check_policy: values.check_policy.clone(),
293            target_triple: values.target_triple.clone(),
294            command_policy: values.command_policy.clone(),
295        }
296    }
297
298    fn paths(&self, family: &LibraryFamily) -> RustPodmanPaths {
299        RustPodmanPaths::new(
300            self.cache.workspace(family),
301            self.cache.cargo_home(),
302            self.cache.target_root(),
303            self.cache.registry_root(),
304            self.cache.cargo_home().join("config.toml"),
305        )
306    }
307
308    fn ensure_config(&self) -> CodingResult<()> {
309        write_config(
310            &self.cache.cargo_home().join("config.toml"),
311            self.registry.cargo_config().as_bytes(),
312        )
313    }
314}
315
316fn read_ordinary(path: &Path) -> CodingResult<Vec<u8>> {
317    let metadata = fs::symlink_metadata(path).map_err(state)?;
318    if metadata.file_type().is_symlink() || !metadata.is_file() {
319        return Err(RustCodingError::State(format!(
320            "not an ordinary nonsymlink file: {}",
321            path.display()
322        )));
323    }
324    fs::read(path).map_err(state)
325}
326
327fn write_config(path: &Path, bytes: &[u8]) -> CodingResult<()> {
328    match fs::symlink_metadata(path) {
329        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
330            return Err(RustCodingError::State(format!(
331                "Cargo configuration is not an ordinary nonsymlink file: {}",
332                path.display()
333            )));
334        }
335        Ok(_) if fs::read(path).map_err(state)? == bytes => return Ok(()),
336        Ok(_) => {}
337        Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
338        Err(cause) => return Err(state(cause)),
339    }
340    let parent = path
341        .parent()
342        .ok_or_else(|| RustCodingError::State("Cargo configuration has no parent".into()))?;
343    let stage = parent.join(format!(".config.toml.stage-{}", std::process::id()));
344    match fs::symlink_metadata(&stage) {
345        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
346            return Err(RustCodingError::State(format!(
347                "Cargo configuration stage is not an ordinary nonsymlink file: {}",
348                stage.display()
349            )));
350        }
351        Ok(_) => fs::remove_file(&stage).map_err(state)?,
352        Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
353        Err(cause) => return Err(state(cause)),
354    }
355    let result = (|| {
356        let mut file = OpenOptions::new()
357            .write(true)
358            .create_new(true)
359            .open(&stage)?;
360        file.write_all(bytes)?;
361        file.sync_all()?;
362        fs::rename(&stage, path)
363    })()
364    .map_err(state);
365    if result.is_err() {
366        let _ = fs::remove_file(stage);
367    }
368    result
369}
370
371fn ensure_directories(root: &Path, names: &[&str]) -> CodingResult<PathBuf> {
372    let mut path = root.to_path_buf();
373    require_directory(&path)?;
374    for name in names {
375        path.push(name);
376        match fs::create_dir(&path) {
377            Ok(()) => {}
378            Err(cause) if cause.kind() == std::io::ErrorKind::AlreadyExists => {
379                require_directory(&path)?;
380            }
381            Err(cause) => return Err(state(cause)),
382        }
383    }
384    Ok(path)
385}
386
387fn require_directory(path: &Path) -> CodingResult<()> {
388    let metadata = fs::symlink_metadata(path).map_err(state)?;
389    if metadata.file_type().is_symlink() || !metadata.is_dir() {
390        return Err(RustCodingError::State(format!(
391            "not an ordinary nonsymlink directory: {}",
392            path.display()
393        )));
394    }
395    Ok(())
396}
397
398fn validate_binary(value: &str) -> CodingResult<()> {
399    let bytes = value.as_bytes();
400    let alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
401    let valid = (1..=36).contains(&bytes.len())
402        && alphanumeric(&bytes[0])
403        && alphanumeric(&bytes[bytes.len() - 1])
404        && bytes.iter().all(|byte| alphanumeric(byte) || *byte == b'-')
405        && !bytes.windows(2).any(|pair| pair == b"--");
406    if valid {
407        Ok(())
408    } else {
409        Err(RustCodingError::Podman(RustPodmanError::InvalidInput {
410            field: "binary",
411            reason: "must be lowercase kebab-case of length 1-36".into(),
412        }))
413    }
414}
415
416fn state(cause: impl Display) -> RustCodingError {
417    RustCodingError::State(cause.to_string())
418}
419
420fn state_result<T>(result: Result<T, String>) -> CodingResult<T> {
421    result.map_err(RustCodingError::State)
422}