Compare commits

...

1 Commits

Author SHA1 Message Date
Conrad Kramer
b555929695 fix: preserve plugin cache symlinks 2026-04-15 10:31:01 -07:00
2 changed files with 82 additions and 2 deletions

View File

@@ -8,6 +8,10 @@ use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::fs;
use std::io;
#[cfg(unix)]
use std::os::unix::fs as unix_fs;
#[cfg(windows)]
use std::os::windows::fs as windows_fs;
use std::path::Path;
use std::path::PathBuf;
@@ -348,21 +352,58 @@ fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreErr
entry.map_err(|err| PluginStoreError::io("failed to enumerate plugin source", err))?;
let source_path = entry.path();
let target_path = target.join(entry.file_name());
let file_type = entry
.file_type()
let metadata = fs::symlink_metadata(&source_path)
.map_err(|err| PluginStoreError::io("failed to inspect plugin source entry", err))?;
let file_type = metadata.file_type();
if file_type.is_dir() {
copy_dir_recursive(&source_path, &target_path)?;
} else if file_type.is_file() {
fs::copy(&source_path, &target_path)
.map_err(|err| PluginStoreError::io("failed to copy plugin file", err))?;
} else if file_type.is_symlink() {
copy_symlink(&source_path, &target_path)?;
}
}
Ok(())
}
#[cfg(unix)]
fn copy_symlink(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
let link_target = fs::read_link(source)
.map_err(|err| PluginStoreError::io("failed to read plugin symlink", err))?;
unix_fs::symlink(link_target, target)
.map_err(|err| PluginStoreError::io("failed to copy plugin symlink", err))
}
#[cfg(windows)]
fn copy_symlink(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
let link_target = fs::read_link(source)
.map_err(|err| PluginStoreError::io("failed to read plugin symlink", err))?;
let resolved_target = if link_target.is_absolute() {
link_target.clone()
} else {
source
.parent()
.map(|parent| parent.join(&link_target))
.unwrap_or_else(|| link_target.clone())
};
let result = if resolved_target.is_dir() {
windows_fs::symlink_dir(link_target, target)
} else {
windows_fs::symlink_file(link_target, target)
};
result.map_err(|err| PluginStoreError::io("failed to copy plugin symlink", err))
}
#[cfg(not(any(unix, windows)))]
fn copy_symlink(_source: &Path, _target: &Path) -> Result<(), PluginStoreError> {
Err(PluginStoreError::Invalid(
"plugin symlinks are not supported on this platform".to_string(),
))
}
#[cfg(test)]
#[path = "store_tests.rs"]
mod tests;

View File

@@ -59,6 +59,45 @@ fn install_copies_plugin_into_default_marketplace() {
assert!(installed_path.join("skills/SKILL.md").is_file());
}
#[cfg(unix)]
#[test]
fn install_preserves_plugin_symlinks() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let source_plugin = tmp.path().join("sample-plugin");
fs::create_dir_all(source_plugin.join("bundle/Versions/A")).unwrap();
fs::write(
source_plugin.join("bundle/Versions/A/payload.txt"),
"payload",
)
.unwrap();
std::os::unix::fs::symlink("A", source_plugin.join("bundle/Versions/Current")).unwrap();
std::os::unix::fs::symlink(
"Versions/Current/payload.txt",
source_plugin.join("bundle/payload.txt"),
)
.unwrap();
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
PluginStore::new(tmp.path().to_path_buf())
.install(AbsolutePathBuf::try_from(source_plugin).unwrap(), plugin_id)
.unwrap();
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
assert_eq!(
fs::read_link(installed_path.join("bundle/Versions/Current")).unwrap(),
PathBuf::from("A"),
);
assert_eq!(
fs::read_link(installed_path.join("bundle/payload.txt")).unwrap(),
PathBuf::from("Versions/Current/payload.txt"),
);
assert_eq!(
fs::read_to_string(installed_path.join("bundle/payload.txt")).unwrap(),
"payload",
);
}
#[test]
fn install_uses_manifest_name_for_destination_and_key() {
let tmp = tempdir().unwrap();