1use crate::{
2 fs::{self, FsError},
3 lockfile::{LockedPackageSpec, OptState},
4 tree::{InstallTree, Tree},
5};
6use std::path::{Path, PathBuf};
7
8pub trait CustomRockLayout: std::fmt::Debug + Send + Sync {
11 fn make_symlinks(&self, tree: &Tree, package: &LockedPackageSpec) -> fs::Result<()>;
12
13 fn remove_symlinks(&self, tree: &Tree, package: &LockedPackageSpec) -> fs::Result<()>;
14}
15
16#[derive(Clone, Debug, Default)]
18pub struct NvimLayout;
19
20impl NvimLayout {
21 fn target_for(tree: &Tree, package: &LockedPackageSpec) -> PathBuf {
22 let subdir = match package.opt() {
23 OptState::Required => "start",
24 OptState::Optional => "opt",
25 };
26 tree.root()
27 .join("site/pack/lux")
28 .join(subdir)
29 .join(package.name().to_string())
30 }
31}
32
33impl CustomRockLayout for NvimLayout {
34 fn make_symlinks(&self, tree: &Tree, package: &LockedPackageSpec) -> fs::Result<()> {
35 let custom_dir = Self::target_for(tree, package);
36 fs::sync::create_dir_all(&custom_dir)?;
37
38 let layout = tree.layout_for(package);
39 try_create_symlink(&layout.src, &custom_dir.join("lua"))?;
40 try_create_symlink(&layout.lib, &custom_dir.join("lib"))?;
41
42 if layout.etc.is_dir() {
43 for entry in fs::sync::read_dir(&layout.etc)? {
44 let entry = entry?;
45 try_create_symlink(&entry.path(), &custom_dir.join(entry.file_name()))?;
46 }
47 }
48
49 Ok(())
50 }
51
52 fn remove_symlinks(&self, tree: &Tree, package: &LockedPackageSpec) -> fs::Result<()> {
53 let target = Self::target_for(tree, package);
54 if target.is_dir() {
55 fs::sync::remove_dir_all(target)?;
57 }
58 Ok(())
59 }
60}
61
62fn try_create_symlink(target: &Path, link: &Path) -> fs::Result<()> {
63 if link.symlink_metadata().is_ok() {
64 return Ok(());
65 }
66
67 let relative = pathdiff::diff_paths(
68 target,
69 link.parent()
70 .ok_or_else(|| unreachable!("missing parent directory"))?,
71 )
72 .ok_or_else(|| {
73 FsError::Other(format!(
74 "failed to compute relative path for symlink from {} to {}",
75 link.display(),
76 target.display()
77 ))
78 })?;
79
80 #[cfg(unix)]
81 fs::unix::symlink(relative, link)?;
82
83 #[cfg(windows)]
84 fs::windows::symlink_dir(relative, link)?;
85
86 Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91 use assert_fs::prelude::PathCopy;
92 use std::path::PathBuf;
93
94 use crate::{
95 config::ConfigBuilder,
96 lockfile::{LockConstraint, LockedPackage, LockedPackageHashes},
97 lua_version::LuaVersion,
98 package::PackageSpec,
99 remote_package_source::RemotePackageSource,
100 tree::{EntryType, InstallTree, NvimLayout},
101 };
102
103 fn mock_hashes() -> LockedPackageHashes {
104 LockedPackageHashes {
105 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
106 .parse()
107 .unwrap(),
108 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
109 .parse()
110 .unwrap(),
111 }
112 }
113
114 fn sample_tree() -> (assert_fs::TempDir, PathBuf, crate::tree::Tree) {
115 let tree_path =
116 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
117 let temp = assert_fs::TempDir::new().unwrap();
118 temp.copy_from(&tree_path, &["**"]).unwrap();
119 let tree_path = temp.to_path_buf();
120 let config = ConfigBuilder::new()
121 .unwrap()
122 .user_tree(Some(tree_path.clone()))
123 .entrypoint_layout(NvimLayout)
124 .build()
125 .unwrap();
126 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
127 (temp, tree_path, tree)
128 }
129
130 fn sample_package() -> LockedPackage {
131 LockedPackage::from(
132 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
133 LockConstraint::Unconstrained,
134 RemotePackageSource::Test,
135 None,
136 mock_hashes(),
137 )
138 }
139
140 #[test]
141 fn nvim_layout_creates_symlinks_for_entrypoints() {
142 let (_temp, tree_path, tree) = sample_tree();
143 let package = sample_package();
144
145 tree.prepare(&package.spec).unwrap();
146 tree.finalize(&package, EntryType::Entrypoint).unwrap();
147
148 let custom_dir = tree_path
149 .join("5.1/site/pack/lux/start")
150 .join(package.name().to_string());
151 assert!(custom_dir.join("lua").symlink_metadata().is_ok());
152 assert!(custom_dir.join("lib").symlink_metadata().is_ok());
153 assert!(custom_dir.join("conf").symlink_metadata().is_err());
154 assert!(custom_dir.join("doc").symlink_metadata().is_err());
155 }
156
157 #[test]
158 fn nvim_layout_links_etc_entries_after_finalize() {
159 let (_temp, tree_path, tree) = sample_tree();
160 let package = sample_package();
161
162 tree.prepare(&package.spec).unwrap();
163
164 let etc = tree.layout_for(&package.spec).etc;
165 std::fs::create_dir_all(etc.join("plugin")).unwrap();
166 std::fs::write(etc.join("plugin/foo.vim"), "lua _G.foo = 1\n").unwrap();
167
168 tree.finalize(&package, EntryType::Entrypoint).unwrap();
169
170 let custom_dir = tree_path
171 .join("5.1/site/pack/lux/start")
172 .join(package.name().to_string());
173 assert!(custom_dir.join("plugin").symlink_metadata().is_ok());
174 }
175
176 #[test]
177 fn nvim_layout_skips_dependencies() {
178 let (_temp, tree_path, tree) = sample_tree();
179 let package = sample_package();
180
181 tree.prepare(&package.spec).unwrap();
182 tree.finalize(&package, EntryType::DependencyOnly).unwrap();
183
184 let custom_dir = tree_path
185 .join("5.1/site/pack/lux/start")
186 .join(package.name().to_string());
187 assert!(!custom_dir.exists());
188 }
189
190 #[test]
191 fn nvim_layout_removes_symlinks() {
192 let (_temp, tree_path, tree) = sample_tree();
193 let package = sample_package();
194
195 tree.prepare(&package.spec).unwrap();
196 tree.finalize(&package, EntryType::Entrypoint).unwrap();
197 let custom_dir = tree_path
198 .join("5.1/site/pack/lux/start")
199 .join(package.name().to_string());
200 assert!(custom_dir.exists());
201
202 tree.cleanup(&package, EntryType::Entrypoint).unwrap();
203 assert!(!custom_dir.exists());
204 }
205}