// 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 { 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 { 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> { 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 { 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;