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}