1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
//! Build-time support for copying plugin libraries into the Cargo output directory.
//!
//! This module provides utilities for integrating plugin libraries into Rust applications
//! at build time. It is designed to be used from `build.rs` scripts to automatically copy
//! plugin dynamic libraries from their build locations into the application's output directory,
//! making them available for runtime loading.
//!
//! # Overview
//!
//! The plugin system requires dynamic libraries to be available at runtime in a known location.
//! This module solves that problem by:
//!
//! 1. Reading plugin declarations from `[package.metadata.plugins]` in `Cargo.toml`
//! 2. Resolving plugin library paths (supporting both absolute and relative paths)
//! 3. Copying plugin libraries to `target/{PROFILE}/plugins/`
//! 4. Emitting `cargo:rerun-if-changed` directives for proper incremental builds
//!
//! # Plugin Declaration Format
//!
//! Plugins are declared in the consuming application's `Cargo.toml` under
//! `[package.metadata.plugins]`. The format supports both individual plugins and
//! grouped plugins:
//!
//! ```toml
//! [package.metadata.plugins]
//! # Individual plugin
//! ssh_plugin = "target/{PROFILE}/libssh_plugin.so"
//!
//! # Grouped plugins
//! [package.metadata.plugins.connection]
//! ssh = "../plugins/ssh/target/{PROFILE}/libssh.so"
//! telnet = "../plugins/telnet/target/{PROFILE}/libtelnet.so"
//!
//! # Absolute paths are also supported
//! [package.metadata.plugins.system]
//! audit = "/opt/genja/plugins/libaudit.so"
//! ```
//!
//! # Path Resolution
//!
//! Plugin paths can be specified in three ways:
//!
//! 1. **Relative paths**: Resolved relative to the manifest directory (where `Cargo.toml` lives)
//! 2. **Absolute paths**: Used as-is without modification
//! 3. **Profile placeholders**: The `{PROFILE}` placeholder is replaced with the current build
//! profile ("debug" or "release")
//!
//! # Build Integration
//!
//! To use this module in your application, add a `build.rs` file to your project root:
//!
//! ```no_run
//! // build.rs
//! fn main() {
//! genja_plugin_manager::build_support::copy_plugins_from_manifest()
//! .expect("Failed to copy plugins");
//! }
//! ```
//!
//! Then declare your plugins in `Cargo.toml`:
//!
//! ```toml
//! [package.metadata.plugins]
//! my_plugin = "target/{PROFILE}/libmy_plugin.so"
//! ```
//!
//! # Runtime Loading
//!
//! After build-time copying, plugins can be loaded at runtime using the plugin manager:
//!
//! ```no_run
//! use genja_plugin_manager::PluginManager;
//!
//! let mut manager = PluginManager::new();
//! manager.load_plugins_from_directory("target/debug/plugins")?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! # Cross-Platform Considerations
//!
//! The helper reads only `[package.metadata.plugins]`, so the plugin path you
//! declare there must use the correct filename for the OS building the
//! application:
//!
//! Linux:
//!
//! ```toml
//! [package.metadata.plugins]
//! my_plugin = "target/{PROFILE}/libmy_plugin.so"
//! ```
//!
//! macOS:
//!
//! ```toml
//! [package.metadata.plugins]
//! my_plugin = "target/{PROFILE}/libmy_plugin.dylib"
//! ```
//!
//! Windows:
//!
//! ```toml
//! [package.metadata.plugins]
//! my_plugin = "target/{PROFILE}/my_plugin.dll"
//! ```
//!
//! # Error Handling
//!
//! All functions in this module return `io::Result<()>`. Common error scenarios include:
//!
//! - Missing environment variables (`CARGO_MANIFEST_DIR`, `OUT_DIR`, `PROFILE`)
//! - Invalid `Cargo.toml` syntax or structure
//! - Missing plugin source files
//! - Permission errors when creating directories or copying files
//! - Invalid plugin path configurations (e.g., paths with no filename)
//!
//! # Examples
//!
//! ## Basic Usage
//!
//! ```no_run
//! // build.rs
//! fn main() {
//! genja_plugin_manager::build_support::copy_plugins_from_manifest()
//! .expect("Failed to copy plugins");
//! }
//! ```
//!
//! ## With Error Handling
//!
//! ```no_run
//! // build.rs
//! fn main() {
//! if let Err(e) = genja_plugin_manager::build_support::copy_plugins_from_manifest() {
//! eprintln!("Warning: Failed to copy plugins: {}", e);
//! eprintln!("Plugins may not be available at runtime");
//! }
//! }
//! ```
//!
//! ## Multiple Plugin Groups
//!
//! ```toml
//! [package.metadata.plugins.connection]
//! ssh = "target/{PROFILE}/libssh.so"
//! telnet = "target/{PROFILE}/libtelnet.so"
//!
//! [package.metadata.plugins.inventory]
//! file = "target/{PROFILE}/libfile_inventory.so"
//! database = "target/{PROFILE}/libdb_inventory.so"
//!
//! [package.metadata.plugins.runner]
//! threaded = "target/{PROFILE}/libthreaded_runner.so"
//! serial = "target/{PROFILE}/libserial_runner.so"
//! ```
//!
//! # Implementation Notes
//!
//! - The module uses `cargo:rerun-if-changed` directives to ensure plugins are recopied
//! when source files change
//! - Plugin directory structure is created automatically if it doesn't exist
//! - Existing plugin files in the destination are overwritten without warning
//! - The module processes nested plugin groups recursively
//! - Profile resolution happens before path resolution, allowing profile-specific paths
use env;
use fs;
use io;
use ;
/// Copy plugin libraries declared in `[package.metadata.plugins]` from the
/// calling application's `Cargo.toml` into `target/{PROFILE}/plugins`.
///
/// This helper is intended to be called from an end-user application's
/// `build.rs`, where `CARGO_MANIFEST_DIR`, `OUT_DIR`, and `PROFILE` all refer
/// to the consuming application rather than a dependency crate.
///
/// # Behavior
///
/// The function reads the `Cargo.toml` manifest from the directory specified by
/// the `CARGO_MANIFEST_DIR` environment variable and looks for plugin entries under
/// `[package.metadata.plugins]`. If found, it copies the specified plugin libraries
/// to a `plugins` subdirectory within the Cargo profile output directory
/// (e.g., `target/debug/plugins` or `target/release/plugins`).
///
/// Plugin paths can contain the `{PROFILE}` placeholder, which will be replaced
/// with the current build profile (e.g., "debug" or "release").
///
/// # Returns
///
/// Returns `Ok(())` if the operation succeeds or if no plugins are declared.
/// Returns an `Err` containing an `io::Error` if:
/// - Required environment variables (`CARGO_MANIFEST_DIR`, `OUT_DIR`, `PROFILE`) are not set
/// - The manifest file cannot be read or parsed
/// - The profile output directory cannot be resolved
/// - Plugin files cannot be copied to the destination
///
/// # Errors
///
/// This function will return an error if:
/// - The `CARGO_MANIFEST_DIR`, `OUT_DIR`, or `PROFILE` environment variables are not set
/// - The `Cargo.toml` file cannot be read or contains invalid TOML
/// - The Cargo profile output directory structure is unexpected
/// - The destination `plugins` directory cannot be created
/// - Any plugin file cannot be copied to the destination
///
/// # Examples
///
/// ```no_run
/// // In your build.rs:
/// fn main() {
/// genja_plugin_manager::build_support::copy_plugins_from_manifest()
/// .expect("Failed to copy plugins");
/// }
/// ```
/// Recursively copies plugin entries from TOML configuration to the plugin directory.
///
/// This function processes plugin entries from the `[package.metadata.plugins]` section
/// of a `Cargo.toml` manifest. It handles both string paths (individual plugin files)
/// and nested tables (groups of plugins), copying each plugin library to the specified
/// plugin directory.
///
/// For string entries, the function:
/// - Replaces `{PROFILE}` placeholders with the actual build profile
/// - Resolves relative paths against the manifest directory
/// - Emits `cargo:rerun-if-changed` directives for build system integration
/// - Copies the plugin file to the destination directory
///
/// For table entries, the function recursively processes all nested values.
///
/// # Parameters
///
/// * `value` - A TOML value representing either a plugin path (string) or a nested
/// table of plugin entries. Must be either a `toml::Value::String` or
/// `toml::Value::Table`.
/// * `manifest_dir` - The directory containing the `Cargo.toml` manifest file. Used
/// as the base directory for resolving relative plugin paths.
/// * `plugin_dir` - The destination directory where plugin libraries should be copied.
/// Typically `target/{PROFILE}/plugins`.
/// * `profile` - The current Cargo build profile (e.g., "debug" or "release"). Used
/// to replace `{PROFILE}` placeholders in plugin paths.
///
/// # Returns
///
/// Returns `Ok(())` if all plugin entries are successfully processed and copied.
/// Returns an `Err` containing an `io::Error` if:
/// - A plugin path is not a valid string or table
/// - A plugin path has no filename component
/// - A plugin file cannot be read or copied
/// - Any nested entry fails to process
///
/// # Errors
///
/// This function will return an error if:
/// - The `value` is neither a string nor a table
/// - A plugin path string has no filename (e.g., ends with `/`)
/// - A source plugin file does not exist or cannot be read
/// - The destination directory is not writable
/// - File copy operations fail for any reason
/// Resolves a plugin source path relative to the manifest directory.
///
/// This function takes a raw path string and resolves it to an absolute path.
/// If the path is already absolute, it is returned as-is. If the path is relative,
/// it is joined with the manifest directory to produce an absolute path.
///
/// # Parameters
///
/// * `manifest_dir` - The base directory containing the `Cargo.toml` manifest file.
/// Used as the reference point for resolving relative paths.
/// * `raw_path` - The raw path string from the plugin configuration. Can be either
/// an absolute path or a relative path.
///
/// # Returns
///
/// Returns a `PathBuf` containing the resolved absolute path. If `raw_path` is
/// absolute, it is returned unchanged. If `raw_path` is relative, it is resolved
/// relative to `manifest_dir`.