Skip to content

Commit 5c2586b

Browse files
committed
fix: preserve plugin cache symlinks
1 parent da86ced commit 5c2586b

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

codex-rs/core/src/plugins/store.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ use serde::Deserialize;
88
use serde_json::Value as JsonValue;
99
use std::fs;
1010
use std::io;
11+
#[cfg(unix)]
12+
use std::os::unix::fs as unix_fs;
13+
#[cfg(windows)]
14+
use std::os::windows::fs as windows_fs;
1115
use std::path::Path;
1216
use std::path::PathBuf;
1317

@@ -348,21 +352,58 @@ fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreErr
348352
entry.map_err(|err| PluginStoreError::io("failed to enumerate plugin source", err))?;
349353
let source_path = entry.path();
350354
let target_path = target.join(entry.file_name());
351-
let file_type = entry
352-
.file_type()
355+
let metadata = fs::symlink_metadata(&source_path)
353356
.map_err(|err| PluginStoreError::io("failed to inspect plugin source entry", err))?;
357+
let file_type = metadata.file_type();
354358

355359
if file_type.is_dir() {
356360
copy_dir_recursive(&source_path, &target_path)?;
357361
} else if file_type.is_file() {
358362
fs::copy(&source_path, &target_path)
359363
.map_err(|err| PluginStoreError::io("failed to copy plugin file", err))?;
364+
} else if file_type.is_symlink() {
365+
copy_symlink(&source_path, &target_path)?;
360366
}
361367
}
362368

363369
Ok(())
364370
}
365371

372+
#[cfg(unix)]
373+
fn copy_symlink(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
374+
let link_target = fs::read_link(source)
375+
.map_err(|err| PluginStoreError::io("failed to read plugin symlink", err))?;
376+
unix_fs::symlink(link_target, target)
377+
.map_err(|err| PluginStoreError::io("failed to copy plugin symlink", err))
378+
}
379+
380+
#[cfg(windows)]
381+
fn copy_symlink(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
382+
let link_target = fs::read_link(source)
383+
.map_err(|err| PluginStoreError::io("failed to read plugin symlink", err))?;
384+
let resolved_target = if link_target.is_absolute() {
385+
link_target.clone()
386+
} else {
387+
source
388+
.parent()
389+
.map(|parent| parent.join(&link_target))
390+
.unwrap_or_else(|| link_target.clone())
391+
};
392+
let result = if resolved_target.is_dir() {
393+
windows_fs::symlink_dir(link_target, target)
394+
} else {
395+
windows_fs::symlink_file(link_target, target)
396+
};
397+
result.map_err(|err| PluginStoreError::io("failed to copy plugin symlink", err))
398+
}
399+
400+
#[cfg(not(any(unix, windows)))]
401+
fn copy_symlink(_source: &Path, _target: &Path) -> Result<(), PluginStoreError> {
402+
Err(PluginStoreError::Invalid(
403+
"plugin symlinks are not supported on this platform".to_string(),
404+
))
405+
}
406+
366407
#[cfg(test)]
367408
#[path = "store_tests.rs"]
368409
mod tests;

codex-rs/core/src/plugins/store_tests.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,48 @@ fn install_copies_plugin_into_default_marketplace() {
5959
assert!(installed_path.join("skills/SKILL.md").is_file());
6060
}
6161

62+
#[cfg(unix)]
63+
#[test]
64+
fn install_preserves_plugin_symlinks() {
65+
let tmp = tempdir().unwrap();
66+
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
67+
let source_plugin = tmp.path().join("sample-plugin");
68+
fs::create_dir_all(source_plugin.join("bundle/Versions/A")).unwrap();
69+
fs::write(
70+
source_plugin.join("bundle/Versions/A/payload.txt"),
71+
"payload",
72+
)
73+
.unwrap();
74+
std::os::unix::fs::symlink("A", source_plugin.join("bundle/Versions/Current")).unwrap();
75+
std::os::unix::fs::symlink(
76+
"Versions/Current/payload.txt",
77+
source_plugin.join("bundle/payload.txt"),
78+
)
79+
.unwrap();
80+
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
81+
82+
PluginStore::new(tmp.path().to_path_buf())
83+
.install(
84+
AbsolutePathBuf::try_from(source_plugin).unwrap(),
85+
plugin_id.clone(),
86+
)
87+
.unwrap();
88+
89+
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
90+
assert_eq!(
91+
fs::read_link(installed_path.join("bundle/Versions/Current")).unwrap(),
92+
PathBuf::from("A"),
93+
);
94+
assert_eq!(
95+
fs::read_link(installed_path.join("bundle/payload.txt")).unwrap(),
96+
PathBuf::from("Versions/Current/payload.txt"),
97+
);
98+
assert_eq!(
99+
fs::read_to_string(installed_path.join("bundle/payload.txt")).unwrap(),
100+
"payload",
101+
);
102+
}
103+
62104
#[test]
63105
fn install_uses_manifest_name_for_destination_and_key() {
64106
let tmp = tempdir().unwrap();

0 commit comments

Comments
 (0)