nushell-plugins/docs/architecture/PLUGIN_EXCLUSION_SYSTEM.md
Jesús Pérez 4b92aa764a
Some checks failed
Build and Test / Validate Setup (push) Has been cancelled
Build and Test / Build (darwin-amd64) (push) Has been cancelled
Build and Test / Build (darwin-arm64) (push) Has been cancelled
Build and Test / Build (linux-amd64) (push) Has been cancelled
Build and Test / Build (windows-amd64) (push) Has been cancelled
Build and Test / Build (linux-arm64) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Package Results (push) Has been cancelled
Build and Test / Quality Gate (push) Has been cancelled
implements a production-ready bootstrap installer with comprehensive error handling, version-agnostic archive extraction, and clear user messaging. All improvements follow DRY principles using symlink-based architecture for single-source-of-truth maintenance
2025-12-11 22:04:54 +00:00

13 KiB

Plugin Exclusion System (v1.0.0)

Overview

The Plugin Exclusion System is a configuration-driven mechanism that allows selective exclusion of plugins from distributions, collections, and installations while maintaining full availability for development, testing, and reference purposes.

Status: Implemented (2025-12-03) Purpose: Exclude reference/documentation plugins (like nu_plugin_example) from end-user distributions


Architecture

Design Principle

Single Source of Truth: All plugin exclusions are centrally defined in etc/plugin_registry.toml, ensuring consistency across all distribution-related operations.

plugin_registry.toml (source of truth)
        ↓
    ┌───┴────────────────────────────────────┐
    ↓                                         ↓
collect_full_binaries.nu      create_distribution_packages.nu
(collection operations)        (packaging operations)
    ↓                                         ↓
distribution/ directories      bin_archives/ packages

Configuration

Plugin Registry Entry

File: etc/plugin_registry.toml

[distribution]
excluded_plugins = [
    "nu_plugin_example"
]
reason = "Reference/documentation plugin - excluded from distributions, installations, and collections. Still included in build and test for validation."

Structure:

  • excluded_plugins (required): List of plugin names to exclude from distributions
  • reason (optional): Documentation of why plugins are excluded

Adding New Exclusions:

[distribution]
excluded_plugins = [
    "nu_plugin_example",
    "nu_plugin_new_reference"  # Add here
]

Implementation

1. Collection System (scripts/collect_full_binaries.nu)

Helper Function

def get_excluded_plugins []: nothing -> list<string> {
    try {
        let registry_path = "./etc/plugin_registry.toml"
        if not ($registry_path | path exists) {
            return []
        }

        let registry_content = open $registry_path
        let excluded = try {
            $registry_content.distribution.excluded_plugins
        } catch {
            []
        }

        return $excluded
    } catch {
        return []
    }
}

Key Features:

  • Reads exclusion list from registry
  • Graceful error handling (returns empty list if registry missing/malformed)
  • Non-blocking (collection continues even if registry unavailable)

Workspace Plugins Filtering

def get_workspace_plugins_info [platform: string, use_release: bool, profile: string]: nothing -> list<record> {
    let excluded_plugins = (get_excluded_plugins)

    let workspace_plugins = [
        "nu_plugin_custom_values"
        "nu_plugin_example"
        "nu_plugin_formats"
        # ... other plugins
    ]

    # Filter out excluded plugins
    let available_plugins = $workspace_plugins | where { |p| $p not-in $excluded_plugins }

    # Process only available plugins
    for plugin in $available_plugins {
        # ... collection logic
    }
}

Custom Plugins Filtering

def get_custom_plugins_info [platform: string, use_release: bool, profile: string]: nothing -> list<record> {
    let excluded_plugins = (get_excluded_plugins)

    let plugin_dirs = (glob $"nu_plugin_*")
        | where ($it | path type) == "dir"
        | where ($it | path basename) != "nushell"
        | where { |p| ($p | path basename) not-in $excluded_plugins }  # Filter excluded
        | each { |p| $p | path basename }

    # Process remaining plugins
}

2. Packaging System (scripts/create_distribution_packages.nu)

Helper Function

def get_excluded_plugins_dist []: nothing -> list<string> {
    try {
        let registry_path = "./etc/plugin_registry.toml"
        if not ($registry_path | path exists) {
            return []
        }

        let registry_content = open $registry_path
        let excluded = try {
            $registry_content.distribution.excluded_plugins
        } catch {
            []
        }

        return $excluded
    } catch {
        return []
    }
}

Plugin Components Filtering

def get_plugin_components [platform: string, version: string] {
    let extension = get_binary_extension $platform
    let excluded_plugins = (get_excluded_plugins_dist)

    # Get custom plugins - skip excluded ones
    let custom_plugin_binaries = (
        glob "nu_plugin_*"
        | where ($it | path type) == "dir"
        | each {|plugin_dir|
            let plugin_name = ($plugin_dir | path basename)
            if $plugin_name in $excluded_plugins {
                null
            } else {
                # ... process plugin
            }
        }
        | compact
    )

    # Get workspace plugins - filter excluded
    let workspace_plugins = [
        "nu_plugin_custom_values"
        "nu_plugin_example"
        # ... other plugins
    ]

    let workspace_plugin_binaries = (
        $workspace_plugins
        | where { |p| $p not-in $excluded_plugins }  # Filter excluded
        | each {|plugin_name|
            # ... process plugin
        }
        | compact
    )

    {
        binaries: ($custom_plugin_binaries | append $workspace_plugin_binaries)
    }
}

3. Installation Configuration (scripts/templates/default_config.nu)

Auto-load Plugin List

Before:

let plugin_binaries = [
    "nu_plugin_clipboard"
    "nu_plugin_desktop_notifications"
    "nu_plugin_hashes"
    "nu_plugin_highlight"
    "nu_plugin_image"
    "nu_plugin_kcl"
    "nu_plugin_tera"
    "nu_plugin_custom_values"
    "nu_plugin_example"          # ❌ Would be auto-loaded
    "nu_plugin_formats"
    # ...
]

After:

# Auto-load common plugins if they're available
# NOTE: nu_plugin_example is excluded from distributions - it's for reference and development only
let plugin_binaries = [
    "nu_plugin_clipboard"
    "nu_plugin_desktop_notifications"
    "nu_plugin_hashes"
    "nu_plugin_highlight"
    "nu_plugin_image"
    "nu_plugin_kcl"
    "nu_plugin_tera"
    "nu_plugin_custom_values"
    "nu_plugin_formats"           # ✅ Auto-loaded (example removed)
    # ...
]

Behavior Matrix

Operation Excluded Plugin Included Plugin
just build Built Built
just build-nushell Built Built
just test Tested Tested
just collect Excluded Collected
just collect-full Excluded Collected
just pack Excluded Packaged
just pack-full Excluded Packaged
Distribution Installation Not auto-loaded Auto-loaded
Manual Reference Use Available Available

Use Cases

Use Case 1: Reference Plugin

Scenario: Plugin serves as a template/documentation reference but shouldn't ship with distributions

Configuration:

[distribution]
excluded_plugins = [
    "nu_plugin_example"
]
reason = "Template for plugin developers. Not intended for end users."

Result:

  • Developers can still use it: ./nushell/target/release/nu_plugin_example
  • End-user distributions don't include it
  • Documentation can reference it as a learning resource

Use Case 2: Experimental Plugin

Scenario: Plugin is under development and not yet stable

Configuration:

[distribution]
excluded_plugins = [
    "nu_plugin_example",
    "nu_plugin_experimental"
]
reason = "Experimental features. Stable once API is finalized."

Result:

  • Can be tested internally
  • Not distributed to users until ready
  • Easily re-enabled by removing from list

Use Case 3: Conditional Exclusion

Scenario: Plugin should only be excluded for specific use cases

Implementation Note: The current system excludes globally. For conditional exclusion, extend the registry:

[distribution]
excluded_plugins = ["nu_plugin_example"]

[distribution.profiles]
enterprise = ["nu_plugin_example", "nu_plugin_dev_tools"]
minimal = ["nu_plugin_example", "nu_plugin_kcl", "nu_plugin_tera"]

Then update scripts to support profile-based filtering.


Error Handling

Scenario: Registry File Missing

Behavior: Scripts return empty exclusion list, all plugins included

if not ($registry_path | path exists) {
    return []  # No exclusions
}

Result: Safe degradation - system works without registry

Scenario: Registry Parse Error

Behavior: Catches exception, returns empty list

let excluded = try {
    $registry_content.distribution.excluded_plugins
} catch {
    []  # If key missing or malformed
}

Result: Malformed registry doesn't break distribution process

Scenario: Invalid Plugin Name

Behavior: Non-existent plugins in exclusion list are silently skipped

| where { |p| $p not-in $excluded_plugins }  # No match = included

Result: Future-proofs against plugin renames or removals


Integration Points

1. Collection Workflow

just collect
  ↓
collect_full_binaries.nu main
  ↓
get_excluded_plugins() → registry.toml
  ↓
get_workspace_plugins_info() → [filtered list]
get_custom_plugins_info() → [filtered list]
  ↓
distribution/ (without excluded plugins)

2. Packaging Workflow

just pack-full
  ↓
create_distribution_packages.nu main
  ↓
get_excluded_plugins_dist() → registry.toml
  ↓
get_plugin_components() → [filtered list]
  ↓
bin_archives/ (without excluded plugins)

3. Build Workflow

just build                    (unchanged)
  ↓
build_all.nu
  ↓
cargo build (all plugins including excluded)
  ↓
target/release/ (includes ALL plugins)

4. Installation Workflow

distribution/platform/
  ↓
default_config.nu (filters excluded at config level)
  ↓
User's Nushell config (excluded plugins not auto-loaded)

Maintenance

Adding a Plugin to Exclusion List

  1. Update Registry:
# Edit: etc/plugin_registry.toml
[distribution]
excluded_plugins = [
    "nu_plugin_example",
    "nu_plugin_new_ref"    # ← Add here
]
  1. Optional: Update Default Config:
# Edit: scripts/templates/default_config.nu
# Add comment explaining why it's excluded
  1. Test:
just collect              # Should exclude both plugins
just pack-full           # Should package without both
just build              # Should still build both

Removing a Plugin from Exclusion List

  1. Update Registry:
# Edit: etc/plugin_registry.toml
[distribution]
excluded_plugins = [
    "nu_plugin_example"    # ← Removed
]
  1. Test:
just collect              # Should now include it
just pack-full           # Should now package it

Files Modified

File Changes Type
etc/plugin_registry.toml Added [distribution] section Config
scripts/collect_full_binaries.nu Added get_excluded_plugins(), updated workspace/custom filtering Feature
scripts/create_distribution_packages.nu Added get_excluded_plugins_dist(), updated component filtering Feature
scripts/templates/default_config.nu Removed excluded plugin from auto-load list Config

Performance Impact

  • Collection: Negligible (single registry read, O(n) filtering where n = excluded count)
  • Packaging: Negligible (same as collection)
  • Build: None (excluded plugins still built)
  • Installation: None (config parsing is same cost)

Future Enhancements

  1. Profile-Based Exclusion: Support different exclusion lists per distribution profile

    [distribution.profiles]
    enterprise = [...]
    minimal = [...]
    
  2. Conditional Compilation: Exclude from build based on feature flags

    #[cfg(feature = "include_example")]
    pub mod example;
    
  3. Deprecation Timeline: Mark plugins as deprecated with removal date

    [distribution.deprecated]
    "nu_plugin_old" = "2025-12-31"  # Will be removed after date
    
  4. Exclusion Reasoning: Rich metadata about why plugins are excluded

    [distribution.exclusions."nu_plugin_example"]
    reason = "reference_plugin"
    since_version = "0.109.0"
    target_inclusion = "never"  # or "1.0.0"
    

References

  • Registry: etc/plugin_registry.toml
  • Collection Scripts: scripts/collect_full_binaries.nu
  • Packaging Scripts: scripts/create_distribution_packages.nu
  • Configuration: scripts/templates/default_config.nu
  • Build System: justfile, justfiles/build.just, scripts/build_all.nu

Testing

Verification Checklist

  • Registry reads correctly: nu -c "open ./etc/plugin_registry.toml | get distribution.excluded_plugins"
  • Collection excludes: just collect && ls distribution/ | grep example (should be empty)
  • Packaging excludes: just pack-full && tar -tzf bin_archives/*.tar.gz | grep example (should be empty)
  • Build includes: just build-nushell && ls nushell/target/release/ | grep example (should exist)
  • Config doesn't auto-load: grep nu_plugin_example scripts/templates/default_config.nu (should not appear in plugin_binaries list)

Version: 1.0.0 Last Updated: 2025-12-03 Status: Stable