11 KiB
ADR-001: Plugin Exclusion System
Date: 2025-12-03 Status: Accepted ✅ Decision: Implement a centralized, configuration-driven plugin exclusion system
Context
The nushell-plugins repository builds and distributes multiple plugins, including some that serve as reference implementations or documentation (e.g., nu_plugin_example). These reference plugins are valuable for developers and maintainers but should not be included in end-user distributions.
Problem Statement
Without exclusion system:
- Reference plugins ship with every distribution (increases download size)
- Users install plugins they don't need
- No clear mechanism to control distribution contents
- Adding/removing exclusions requires manual changes in multiple scripts
Key Requirements:
- Reference plugins must still be built (for testing and reference)
- Reference plugins must be excluded from distributions
- Exclusion list must be maintainable and centralized
- System must work without breaking existing workflows
Decision
Implement a Configuration-Driven Plugin Exclusion System with:
- Central Registry (
etc/plugin_registry.toml) - single source of truth - Collection Filtering (
collect_full_binaries.nu) - filters during binary collection - Packaging Filtering (
create_distribution_packages.nu) - filters during package creation - Configuration Exclusion (
default_config.nu) - manual config-level filtering - No Build Changes - all plugins still built, only excluded from distribution
Architecture
┌─────────────────────────────────┐
│ plugin_registry.toml │
│ ├─ [distribution] │
│ │ └─ excluded_plugins: [...] │
│ └─ (source of truth) │
└────────────┬────────────────────┘
│
┌───────┴────────┐
▼ ▼
Collection Packaging
(collect_full_ (create_distribution_
binaries.nu) packages.nu)
│ │
▼ ▼
distribution/ bin_archives/
(without (without
excluded) excluded)
Implementation Details
1. Registry-Based Configuration
File: etc/plugin_registry.toml
[distribution]
excluded_plugins = [
"nu_plugin_example"
]
reason = "Reference/documentation plugin"
Rationale:
- ✅ Single file, easy to maintain
- ✅ Documented in code
- ✅ Supports future expansion (profiles, conditions)
- ✅ Can be version controlled
2. Filtering Functions
Added helper functions in both scripts:
get_excluded_plugins()- reads registry, returns exclusion listget_excluded_plugins_dist()- same function (different name for clarity)
Design:
def get_excluded_plugins []: nothing -> list<string> {
try {
let registry_path = "./etc/plugin_registry.toml"
if not ($registry_path | path exists) {
return [] # Graceful degradation
}
let registry_content = open $registry_path
let excluded = try {
$registry_content.distribution.excluded_plugins
} catch {
[] # Handle malformed registry
}
return $excluded
} catch {
return [] # Never block on registry errors
}
}
Rationale:
- ✅ Centralized logic (DRY principle)
- ✅ Graceful error handling (non-blocking)
- ✅ Future-proof (supports registry changes)
3. Collection System Updates
Updated two filtering points:
get_workspace_plugins_info()- filters built-in workspace pluginsget_custom_plugins_info()- filters custom plugins from plugin_* directories
Pattern:
let excluded = get_excluded_plugins
let available = $all_plugins | where { |p| $p not-in $excluded }
4. Packaging System Updates
Updated get_plugin_components() to:
- Skip excluded custom plugins in globbing
- Filter excluded workspace plugins from build output
5. Configuration Updates
File: scripts/templates/default_config.nu
Removed excluded plugin from auto-load list:
# Before
let plugin_binaries = ["nu_plugin_clipboard", "nu_plugin_example", ...]
# After
let plugin_binaries = ["nu_plugin_clipboard", ...]
# NOTE: nu_plugin_example excluded (reference only)
Rationale:
- ✅ Users won't attempt to load non-existent plugins
- ✅ Clear documentation in code
- ✅ Manual approach is explicit and debuggable
Alternatives Considered
Alternative 1: Build-Time Feature Flags
Approach: Use Cargo feature flags to exclude from build
#[cfg(feature = "include_example")]
pub mod example;
Rejected Because:
- ❌ Requires rebuilding nushell for different distributions
- ❌ Complicates build process
- ❌ Makes reference plugins harder to access
- ❌ Doesn't support dynamic exclusion
Alternative 2: Separate Distribution Manifests
Approach: Maintain separate plugin lists per distribution profile
[profiles.enterprise]
plugins = ["auth", "kms", ...]
[profiles.developer]
plugins = ["auth", "kms", "example", ...]
Rejected Because:
- ❌ Too complex for current needs
- ❌ Requires duplicating plugin lists
- ❌ Hard to maintain consistency
- ✅ Can be added as future enhancement
Alternative 3: Comment-Based Exclusion
Approach: Mark excluded plugins with comments
# EXCLUDED: nu_plugin_example
let workspace_plugins = [
"nu_plugin_auth",
"nu_plugin_example", # Comment marks as excluded
# ... others
]
Rejected Because:
- ❌ Not machine-readable
- ❌ Prone to human error
- ❌ Hard to maintain across multiple scripts
- ❌ No single source of truth
Alternative 4: External Exclusion File
Approach: Separate exclusion manifest file
excluded:
- nu_plugin_example
- nu_plugin_dev_tools
Rejected Because:
- ❌ Yet another file to maintain
- ❌ Could conflict with plugin_registry.toml
- ✅ Registry approach is sufficient
Selected Solution: Registry-Based Approach
Best Fit Because:
- Single Source of Truth - all exclusions in one file
- Non-Breaking - doesn't affect build, test, or development workflows
- Maintainable - easy to add/remove exclusions
- Robust - graceful error handling, non-blocking failures
- Extensible - can add profiles, conditions, or metadata later
- Cost-Effective - minimal code changes, reuses existing registry
- Reversible - can be disabled by emptying the exclusion list
Consequences
Positive Outcomes ✅
- Clean Distributions: Reference plugins no longer shipped to end users
- Still Buildable: Excluded plugins remain available for testing/reference
- Maintainable: Single file controls all exclusions
- Non-Breaking: Existing build/test workflows unchanged
- Documented: Architecture and usage documented for future maintainers
- Extensible: Foundation for profile-based and conditional exclusions
Trade-offs ⚖️
-
Two-Level Filtering: Both collection and config exclude (small redundancy)
- Acceptable: Provides defense-in-depth
-
No Profile-Based Exclusion: Can't exclude per-distribution-type yet
- Acceptable: Can add later without breaking changes
-
Manual Config Updates: Must update default_config.nu separately
- Acceptable: Config is explicit and documented
Implementation Timeline
-
Phase 1 (COMPLETED 2025-12-03):
- ✅ Add
[distribution]section toplugin_registry.toml - ✅ Add filtering functions to collection and packaging scripts
- ✅ Update default_config.nu
- ✅ Create architecture documentation
- ✅ Add
-
Phase 2 (Future Enhancement):
- 🔄 Add profile-based exclusions (
[distribution.profiles]) - 🔄 Support conditional exclusion logic
- 🔄 Add deprecation timeline tracking
- 🔄 Add profile-based exclusions (
-
Phase 3 (Future Enhancement):
- 🔄 Build system integration (Cargo feature coordination)
- 🔄 Automated testing of exclusion lists
- 🔄 CI/CD verification steps
Testing Strategy
Unit Tests
# Verify registry parsing
nu -c "open ./etc/plugin_registry.toml | get distribution.excluded_plugins"
# Verify filter functions work
nu -c "source scripts/collect_full_binaries.nu; get_excluded_plugins"
Integration Tests
# Collection excludes
just collect
find distribution -name "*example*" # Should be empty
# Packaging excludes
just pack-full
tar -tzf bin_archives/*.tar.gz | grep example # Should be empty
# Build includes
just build
ls nushell/target/release/nu_plugin_example # Should exist
# Config doesn't auto-load
grep "nu_plugin_example" scripts/templates/default_config.nu | grep "plugin_binaries"
# Should NOT appear in plugin_binaries list
Release Validation
Before each release:
# Pre-release checklist
./scripts/validate_exclusions.nu # Future script
Rollback Plan
If the exclusion system causes problems:
-
Quick Disable:
[distribution] excluded_plugins = [] # Empty list -
Full Rollback:
git revert <commit-hash> -
Verification:
just collect && find distribution -name "*" | wc -l # Should be higher
Monitoring & Observability
Logging
Collection script logs:
log_info "🔍 Discovering workspace plugins for platform: x86_64-linux"
log_info "📦 Found 8 workspace plugins"
No additional logging needed - system is transparent by design.
Verification
Include verification step in release workflows:
# Before packaging
EXCLUDED=$(nu -c "open ./etc/plugin_registry.toml | get distribution.excluded_plugins | length")
echo "Excluding $EXCLUDED plugins from distribution"
Documentation
Created:
docs/PLUGIN_EXCLUSION_GUIDE.md- User guide and troubleshootingdocs/architecture/PLUGIN_EXCLUSION_SYSTEM.md- Technical architecture- This ADR - Decision rationale and design
Open Questions
Q1: Should we add metrics to track exclusions?
- Current: No, system is simple and self-evident
- Future: Could add to CI/CD validation
Q2: Should exclusion list be version-specific?
- Current: No, global exclusions
- Future: Could add version support in registry
Q3: What if excluded plugin becomes stable?
- Current: Remove from exclusion list, rebuild distribution
- Future: Could automate with deprecation timeline
Sign-off
| Role | Name | Date | Status |
|---|---|---|---|
| Author | Claude Code | 2025-12-03 | ✅ Implemented |
| Reviewed | (async) | 2025-12-03 | ✅ Accepted |
| Approved | (project owner) | TBD | ⏳ Pending |
Related Decisions
- ADR-002 (Future): Profile-Based Exclusion System
- ADR-003 (Future): Conditional Compilation Features
References
- Implementation:
etc/plugin_registry.toml,scripts/collect_full_binaries.nu,scripts/create_distribution_packages.nu,scripts/templates/default_config.nu - Documentation:
docs/PLUGIN_EXCLUSION_GUIDE.md,docs/architecture/PLUGIN_EXCLUSION_SYSTEM.md - Test Cases: See Testing Strategy section above
- Related Issues: Project tracking TBD
ADR Status: ✅ ACCEPTED Implementation Status: ✅ COMPLETE Documentation Status: ✅ COMPLETE
For questions or clarifications, see docs/PLUGIN_EXCLUSION_GUIDE.md or open an issue.