168 lines
8.0 KiB
Rust
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;
|