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}