Files
khadhroony-solana-project/crates/ksp-config-lib/src/packaging.rs
2026-08-22 10:56:26 +02:00

168 lines
8.0 KiB
Rust

// file: crates/ksp-config-lib/src/packaging.rs
// version: 1
//! Packaged desktop runtime layout owned by Config.
const PACKAGED_PROJECT_APPLICATION: &str = "khadhroony-solana-project";
const PACKAGED_PROJECT_ORGANIZATION: &str = "SASEDEV";
const PACKAGED_PROJECT_QUALIFIER: &str = "com";
/// Writable KSP runtime roots prepared from packaged Config resources.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackagedRuntimeLayout {
runtime_root: std::path::PathBuf,
cfg_path: std::path::PathBuf,
schema_path: std::path::PathBuf,
}
impl PackagedRuntimeLayout {
/// Returns the shared writable KSP runtime root used as the packaged process working directory.
#[must_use]
pub fn runtime_root(&self) -> &std::path::Path {
return self.runtime_root.as_path();
}
/// Returns the writable root containing Config-managed runtime documents.
#[must_use]
pub fn cfg_path(&self) -> &std::path::Path {
return self.cfg_path.as_path();
}
/// Returns the writable root containing the package-owned current JSON Schemas.
#[must_use]
pub fn schema_path(&self) -> &std::path::Path {
return self.schema_path.as_path();
}
}
/// Prepares the shared writable KSP desktop runtime from immutable packaged resources.
///
/// Runtime Config documents are seeded only when absent so existing user-managed sources are never silently overwritten. JSON Schemas are package-owned
/// authority and are synchronized on each packaged launch. The local `.env`, Logging outputs and Wallet roots remain ordinary relative runtime resources
/// below the returned shared root unless their managed Config explicitly selects another path.
pub fn prepare_packaged_runtime(resource_root: &std::path::Path) -> ksp_core_lib::Result<PackagedRuntimeLayout> {
let project_dirs = directories::ProjectDirs::from(PACKAGED_PROJECT_QUALIFIER, PACKAGED_PROJECT_ORGANIZATION, PACKAGED_PROJECT_APPLICATION);
let project_dirs = match project_dirs {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED,
"Cannot resolve the writable packaged KSP runtime directory",
));
},
};
return prepare_packaged_runtime_at(resource_root, project_dirs.data_dir());
}
fn prepare_packaged_runtime_at(resource_root: &std::path::Path, runtime_root: &std::path::Path) -> ksp_core_lib::Result<PackagedRuntimeLayout> {
let packaged_cfg_path = resource_root.join(crate::DEFAULT_CFG_PATH);
let packaged_schema_path = resource_root.join(crate::DEFAULT_SCHEMA_PATH);
let cfg_path = runtime_root.join(crate::DEFAULT_CFG_PATH);
let schema_path = runtime_root.join(crate::DEFAULT_SCHEMA_PATH);
let runtime_creation = std::fs::create_dir_all(runtime_root);
if let std::result::Result::Err(error) = runtime_creation {
return std::result::Result::Err(packaging_io_error(runtime_root, "packaged KSP runtime root cannot be created", error));
}
let cfg_creation = std::fs::create_dir_all(cfg_path.as_path());
if let std::result::Result::Err(error) = cfg_creation {
return std::result::Result::Err(packaging_io_error(cfg_path.as_path(), "packaged Config root cannot be created", error));
}
let schema_creation = std::fs::create_dir_all(schema_path.as_path());
if let std::result::Result::Err(error) = schema_creation {
return std::result::Result::Err(packaging_io_error(schema_path.as_path(), "packaged Config schema root cannot be created", error));
}
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for descriptor in registry.descriptors() {
let (source_root, destination_root) = match descriptor.kind() {
crate::ConfigFileKind::Config => (packaged_cfg_path.as_path(), cfg_path.as_path()),
crate::ConfigFileKind::Schema => (packaged_schema_path.as_path(), schema_path.as_path()),
};
let source = source_root.join(descriptor.filename());
let destination = destination_root.join(descriptor.filename());
let content = read_packaged_resource(source.as_path());
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persistence = match descriptor.kind() {
crate::ConfigFileKind::Config => seed_config_document(destination.as_path(), content.as_slice()),
crate::ConfigFileKind::Schema => synchronize_schema(destination.as_path(), content.as_slice()),
};
if let std::result::Result::Err(error) = persistence {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(PackagedRuntimeLayout { runtime_root: runtime_root.to_path_buf(), cfg_path, schema_path });
}
fn read_packaged_resource(path: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let metadata = std::fs::symlink_metadata(path);
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(packaging_io_error(path, "packaged Config resource metadata cannot be read", error));
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return std::result::Result::Err(packaging_error(path, "packaged Config resource must be a regular non-symlink file"));
}
let content = std::fs::read(path);
return match content {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(packaging_io_error(path, "packaged Config resource cannot be read", error)),
};
}
fn seed_config_document(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
let existing = destination_state(path);
let existing = match existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if existing {
return std::result::Result::Ok(());
}
return crate::atomic_write(path, content);
}
fn synchronize_schema(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
let existing = destination_state(path);
if let std::result::Result::Err(error) = existing {
return std::result::Result::Err(error);
}
return crate::atomic_write(path, content);
}
fn destination_state(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
let metadata = std::fs::symlink_metadata(path);
return match metadata {
std::result::Result::Ok(value) => {
if value.file_type().is_symlink() || !value.is_file() {
std::result::Result::Err(packaging_error(path, "packaged runtime destination must be a regular non-symlink file when it exists"))
} else {
std::result::Result::Ok(true)
}
},
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(false),
std::result::Result::Err(error) => std::result::Result::Err(packaging_io_error(path, "packaged runtime destination metadata cannot be read", error)),
};
}
fn packaging_error(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED, "Packaged KSP runtime preparation failed")
.with_context("path", path.to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn packaging_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
return packaging_error(path, reason).with_source(source);
}
#[cfg(test)]
#[path = "../unit_tests/packaging.rs"]
mod tests;