Skip to content

Settings

Configuration management for Brainsmith projects with hierarchical loading and type-safe validation using Pydantic.

Supports loading from CLI arguments, environment variables (BSMITH_* prefix), project config files (brainsmith.yaml), and built-in defaults.


load_config

load_config(project_file: Path | None = None, **cli_overrides) -> SystemConfig

Load configuration with hierarchical priority.

Priority order (highest to lowest): 1. CLI arguments (passed as kwargs) 2. Environment variables (BSMITH_* prefix) 3. Project config file (brainsmith.yaml) 4. Built-in defaults

Special handling: - BSMITH_LOG_LEVEL env var overrides logging.level (shorthand for BSMITH_LOGGING__LEVEL)

Path Resolution: - Absolute paths: Used as-is - Relative CLI paths: Resolve to current working directory - Relative paths (YAML/env/defaults): Resolve to project directory

Parameters:

Name Type Description Default
project_file Path | None

Path to project config file (for non-standard locations)

None
**cli_overrides

CLI argument overrides

{}

Returns:

Type Description
SystemConfig

SystemConfig object

Source code in brainsmith/settings/loader.py
def load_config(project_file: Path | None = None, **cli_overrides) -> SystemConfig:
    """Load configuration with hierarchical priority.

    Priority order (highest to lowest):
    1. CLI arguments (passed as kwargs)
    2. Environment variables (BSMITH_* prefix)
    3. Project config file (brainsmith.yaml)
    4. Built-in defaults

    Special handling:
    - BSMITH_LOG_LEVEL env var overrides logging.level (shorthand for BSMITH_LOGGING__LEVEL)

    Path Resolution:
    - Absolute paths: Used as-is
    - Relative CLI paths: Resolve to current working directory
    - Relative paths (YAML/env/defaults): Resolve to project directory

    Args:
        project_file: Path to project config file (for non-standard locations)
        **cli_overrides: CLI argument overrides

    Returns:
        SystemConfig object
    """
    try:
        cli_overrides = _resolve_cli_paths(cli_overrides)

        # Handle BSMITH_LOG_LEVEL shorthand (if not overridden by CLI)
        if "logging" not in cli_overrides and "BSMITH_LOG_LEVEL" in os.environ:
            log_level = os.environ["BSMITH_LOG_LEVEL"]
            cli_overrides["logging"] = {"level": log_level}

        if project_file:
            cli_overrides["_project_file"] = project_file

        return SystemConfig(**cli_overrides)

    except ValidationError as e:
        console.print("[bold red]Configuration validation failed:[/bold red]")
        for error in e.errors():
            field = " → ".join(str(x) for x in error["loc"])
            console.print(f"  [red]{field}: {error['msg']}[/red]")
        raise

Example:

from brainsmith.settings import load_config

# Load with default project config
config = load_config()

# Load with custom project file
config = load_config(project_file="custom.yaml")

# Load with CLI overrides
config = load_config(
    build_dir="./custom-build",
    vivado_path="/tools/Xilinx/Vivado/2024.2"
)

get_config cached

get_config() -> SystemConfig

Get cached configuration instance.

Environment must be sourced first

source .brainsmith/env.sh # or: direnv allow

Source code in brainsmith/settings/loader.py
@lru_cache(maxsize=1)
def get_config() -> SystemConfig:
    """Get cached configuration instance.

    Environment must be sourced first:
        source .brainsmith/env.sh  # or: direnv allow
    """
    config = load_config()
    return config

Example:

from brainsmith.settings import get_config

# Get cached config instance
config = get_config()

# Access configuration values
print(config.build_dir)
print(config.vivado_path)
print(config.logging.level)

get_default_config

get_default_config() -> SystemConfig

Get a configuration instance with only default values (no files or env vars).

Source code in brainsmith/settings/loader.py
def get_default_config() -> SystemConfig:
    """Get a configuration instance with only default values (no files or env vars)."""
    filtered_env = {k: v for k, v in os.environ.items() if not k.startswith("BSMITH_")}

    with patch.dict(os.environ, filtered_env, clear=True):
        # Prevent loading config files
        from pathlib import Path

        return load_config(project_file=Path("/dev/null"))

Example:

from brainsmith.settings import get_default_config

# Get default config without loading from files/env
default_config = get_default_config()

SystemConfig

Bases: BaseSettings

Configuration schema with hierarchical priority.

Priority order (highest to lowest): 1. CLI arguments (passed to constructor) 2. Environment variables (BSMITH_* prefix) 3. Project config (brainsmith.yaml) 4. Built-in defaults

bsmith_dir cached property

bsmith_dir: Path

Brainsmith repository root containing pyproject.toml.

This is the parent of the brainsmith package directory.

generate_activation_script

generate_activation_script(output_path: Path) -> Path

Generate bash activation script from current configuration.

The script can be sourced multiple times safely: - Cleans up old Xilinx/brainsmith paths before adding new ones - Sources Xilinx settings64.sh files for complete tool environment

Parameters:

Name Type Description Default
output_path Path

Where to write the activation script

required

Returns:

Type Description
Path

Path to generated script

Example

config = get_config() config.generate_activation_script(Path("~/.brainsmith/env.sh"))

User runs: source ~/.brainsmith/env.sh

Source code in brainsmith/settings/schema.py
def generate_activation_script(self, output_path: Path) -> Path:
    """Generate bash activation script from current configuration.

    The script can be sourced multiple times safely:
    - Cleans up old Xilinx/brainsmith paths before adding new ones
    - Sources Xilinx settings64.sh files for complete tool environment

    Args:
        output_path: Where to write the activation script

    Returns:
        Path to generated script

    Example:
        >>> config = get_config()
        >>> config.generate_activation_script(Path("~/.brainsmith/env.sh"))
        >>> # User runs: source ~/.brainsmith/env.sh
    """
    from .env_export import EnvironmentExporter

    env_dict = EnvironmentExporter(self).to_env_dict()

    script_lines = [
        "#!/bin/bash",
        "# Auto-generated by brainsmith",
        "# Source this file to set up environment:",
        "#   source .brainsmith/env.sh",
        "",
        "# This script is idempotent - safe to source multiple times",
        "",
        self._generate_cleanup_code(),
        "",
        "# Export fresh environment variables",
    ]

    for key, value in sorted(env_dict.items()):
        # Skip internal markers
        if key.startswith("_BRAINSMITH") or key.startswith("_OLD_"):
            continue

        # Skip PATH - we'll add Xilinx tool paths separately
        if key == "PATH":
            continue

        # Properly escape quotes in values
        escaped_value = str(value).replace('"', '\\"')
        script_lines.append(f'export {key}="{escaped_value}"')

    # Add Xilinx tool paths to PATH
    script_lines.extend(
        [
            "",
            "# Add Xilinx tools to PATH",
            'if [ -n "$VIVADO_PATH" ]; then',
            '    export PATH="$VIVADO_PATH/bin:$PATH"',
            "fi",
            "",
            'if [ -n "$VITIS_PATH" ]; then',
            '    export PATH="$VITIS_PATH/bin:$PATH"',
            "fi",
            "",
            'if [ -n "$HLS_PATH" ]; then',
            '    export PATH="$HLS_PATH/bin:$PATH"',
            "fi",
        ]
    )

    # Source Xilinx settings64.sh for complete environment
    script_lines.extend(
        [
            "",
            "# Source Xilinx tool settings for full environment",
            "# Vitis includes Vivado, so check it first",
            'if [ -n "$VITIS_PATH" ] && [ -f "$VITIS_PATH/settings64.sh" ]; then',
            '    source "$VITIS_PATH/settings64.sh" 2>/dev/null',
            'elif [ -n "$VIVADO_PATH" ] && [ -f "$VIVADO_PATH/settings64.sh" ]; then',
            '    source "$VIVADO_PATH/settings64.sh" 2>/dev/null',
            "fi",
            "",
            "# Source HLS separately (not included in Vitis)",
            'if [ -n "$HLS_PATH" ] && [ -f "$HLS_PATH/settings64.sh" ]; then',
            '    source "$HLS_PATH/settings64.sh" 2>/dev/null',
            "fi",
        ]
    )

    output_path = Path(output_path).expanduser()
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text("\n".join(script_lines))
    output_path.chmod(0o755)

    return output_path

generate_direnv_file

generate_direnv_file(output_path: Path) -> Path

Generate .envrc file for direnv integration.

Creates a direnv configuration that: - Watches brainsmith.yaml for changes - Auto-regenerates environment when config changes - Sources .brainsmith/env.sh for all environment variables - Activates virtualenv automatically

User must run 'direnv allow' to trust the file.

Parameters:

Name Type Description Default
output_path Path

Where to write the .envrc file (typically project root)

required

Returns:

Type Description
Path

Path to generated .envrc file

Example

config = get_config() config.generate_direnv_file(Path(".envrc"))

User runs: direnv allow

Source code in brainsmith/settings/schema.py
    def generate_direnv_file(self, output_path: Path) -> Path:
        """Generate .envrc file for direnv integration.

        Creates a direnv configuration that:
        - Watches brainsmith.yaml for changes
        - Auto-regenerates environment when config changes
        - Sources .brainsmith/env.sh for all environment variables
        - Activates virtualenv automatically

        User must run 'direnv allow' to trust the file.

        Args:
            output_path: Where to write the .envrc file (typically project root)

        Returns:
            Path to generated .envrc file

        Example:
            >>> config = get_config()
            >>> config.generate_direnv_file(Path(".envrc"))
            >>> # User runs: direnv allow
        """
        output_path.parent / ".brainsmith"

        envrc_content = """#!/usr/bin/env bash
# Auto-generated by brainsmith
# Enable with: direnv allow

# Watch config file - direnv will reload when it changes
watch_file brainsmith.yaml

# Activate virtualenv first (required for brainsmith command)
if [ -d .venv ]; then
    export VIRTUAL_ENV="$PWD/.venv"
    PATH_add "$VIRTUAL_ENV/bin"
fi

# Auto-regenerate environment if config is newer than env.sh
if [ brainsmith.yaml -nt .brainsmith/env.sh ]; then
    echo "Config changed, regenerating environment..."
    if command -v brainsmith &> /dev/null; then
        brainsmith project init > /dev/null 2>&1 || {
            echo -e "\033[33mFailed to regenerate. Run: brainsmith project init\033[0m"
        }
    else
        echo -e "\033[33mbrainsmith command not found. Check venv activation.\033[0m"
    fi
fi

# Source Brainsmith environment (sets all variables, sources Xilinx settings64.sh)
source_env .brainsmith/env.sh
"""

        output_path = Path(output_path).expanduser()
        output_path.write_text(envrc_content)
        output_path.chmod(0o644)  # Readable but not executable (direnv sources it)

        return output_path

model_post_init

model_post_init(__context: Any) -> None

Resolve all paths to absolute.

At this point: - CLI paths are already absolute (resolved to CWD in load_config) - YAML/env/default paths may be relative (need resolution to project_dir)

Resolution steps: 1. Detect project_dir (where config file is, or CWD) 2. Resolve user-facing paths (relative → project_dir) 3. Force internal paths (deps_dir → bsmith_dir) 4. Set defaults for unset paths 5. Validate everything is sane 6. Check for deprecated configuration

Source code in brainsmith/settings/schema.py
def model_post_init(self, __context: Any) -> None:
    """Resolve all paths to absolute.

    At this point:
    - CLI paths are already absolute (resolved to CWD in load_config)
    - YAML/env/default paths may be relative (need resolution to project_dir)

    Resolution steps:
    1. Detect project_dir (where config file is, or CWD)
    2. Resolve user-facing paths (relative → project_dir)
    3. Force internal paths (deps_dir → bsmith_dir)
    4. Set defaults for unset paths
    5. Validate everything is sane
    6. Check for deprecated configuration
    """
    self.project_dir = self._detect_project_root()
    self._resolve_core_paths()
    self._resolve_xilinx_tools()
    self._resolve_finn_paths()
    self._resolve_component_sources()
    self._resolve_source_priority()
    self._check_deprecations()

settings_customise_sources classmethod

settings_customise_sources(settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource) -> tuple[PydanticBaseSettingsSource, ...]

Customize settings sources.

Priority order (first source wins): 1. Init settings (CLI/constructor args) - paths already resolved to CWD 2. Environment variables (BSMITH_*) - paths stay relative, resolved in model_post_init 3. YAML files (custom source) - paths stay relative, resolved in model_post_init 4. Field defaults (built into pydantic)

Path Resolution: - CLI paths are resolved to CWD in load_config() before reaching here - All other paths stay relative and are resolved in model_post_init()

Source code in brainsmith/settings/schema.py
@classmethod
def settings_customise_sources(
    cls,
    settings_cls: type[BaseSettings],
    init_settings: PydanticBaseSettingsSource,
    env_settings: PydanticBaseSettingsSource,
    dotenv_settings: PydanticBaseSettingsSource,
    file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
    """Customize settings sources.

    Priority order (first source wins):
    1. Init settings (CLI/constructor args) - paths already resolved to CWD
    2. Environment variables (BSMITH_*) - paths stay relative, resolved in model_post_init
    3. YAML files (custom source) - paths stay relative, resolved in model_post_init
    4. Field defaults (built into pydantic)

    Path Resolution:
    - CLI paths are resolved to CWD in load_config() before reaching here
    - All other paths stay relative and are resolved in model_post_init()
    """
    # Extract file paths from init_settings if provided
    init_dict = init_settings()
    project_file = init_dict.get("_project_file")

    return (
        init_settings,  # CLI args (already resolved to CWD in load_config)
        env_settings,  # Env vars (stay relative, resolved in model_post_init)
        YamlSettingsSource(settings_cls, project_file=project_file),
    )

validate_component_sources classmethod

validate_component_sources(v: Any) -> dict[str, Path | None]

Validate component sources and warn about reserved source names.

Reserved source names: - Core namespace ('brainsmith'): Internal components loaded via direct import - Entry points (e.g., 'finn'): Discovered via pip package entry points

Users can configure filesystem-based sources (project, user, custom) but cannot override reserved names.

Source code in brainsmith/settings/schema.py
@field_validator("component_sources", mode="before")
@classmethod
def validate_component_sources(cls, v: Any) -> dict[str, Path | None]:
    """Validate component sources and warn about reserved source names.

    Reserved source names:
    - Core namespace ('brainsmith'): Internal components loaded via direct import
    - Entry points (e.g., 'finn'): Discovered via pip package entry points

    Users can configure filesystem-based sources (project, user, custom) but
    cannot override reserved names.
    """
    import logging

    logger = logging.getLogger(__name__)

    default_sources = cls.model_fields["component_sources"].default_factory()

    if v is None or not isinstance(v, dict):
        return default_sources

    result = default_sources.copy()

    for key, value in v.items():
        # Warn if trying to override core namespace
        if key == CORE_NAMESPACE and value is not None:
            logger.warning(
                f"Component source '{key}' is a reserved core namespace and cannot be "
                f"configured. The core brainsmith components are loaded from the package "
                f"installation automatically. This configuration will be ignored."
            )
            continue  # Skip, don't add to result

        # Warn if trying to override known entry point sources
        if key in KNOWN_ENTRY_POINTS and value is not None:
            logger.warning(
                f"Component source '{key}' is a registered entry point and cannot be "
                f"configured. Entry point sources are discovered automatically from "
                f"installed packages. This configuration will be ignored."
            )
            continue  # Skip, don't add to result

        # Add custom or standard filesystem sources
        result[key] = Path(value) if isinstance(value, str) else value

    return result

Example:

from brainsmith.settings import SystemConfig

# Create config with custom values
config = SystemConfig(
    build_dir="./build",
    vivado_path="/tools/Xilinx/Vivado/2024.2"
)

# Access nested configuration
print(config.logging.level)
print(config.netron_port)

EnvironmentExporter

EnvironmentExporter(config: SystemConfig)

Export configuration as environment variables for shell scripts.

Generates environment variable dictionaries for: - FINN (FINN_ROOT, FINN_BUILD_DIR, etc.) - Xilinx tools (VIVADO_PATH, XILINX_VIVADO, etc.) - Visualization tools (NETRON_PORT) - BSMITH_* variables (for YAML ${var} expansion in blueprints)

Example

config = SystemConfig() exporter = EnvironmentExporter(config) env_dict = exporter.to_external_dict() print(env_dict['FINN_ROOT'])

Initialize with system configuration.

Source code in brainsmith/settings/env_export.py
def __init__(self, config: "SystemConfig"):
    """Initialize with system configuration."""
    self.config = config

to_all_dict

to_all_dict() -> dict[str, str]

Generate dict of all environment variables.

Includes both external tool variables and internal BSMITH_* variables.

Returns:

Type Description
dict[str, str]

Dict of all environment variables

Source code in brainsmith/settings/env_export.py
def to_all_dict(self) -> dict[str, str]:
    """Generate dict of all environment variables.

    Includes both external tool variables and internal BSMITH_* variables.

    Returns:
        Dict of all environment variables
    """
    env_dict = self.to_external_dict()

    env_dict["BSMITH_BUILD_DIR"] = str(self.config.build_dir)
    env_dict["BSMITH_DEPS_DIR"] = str(self.config.deps_dir)
    env_dict["BSMITH_DIR"] = str(self.config.bsmith_dir)
    env_dict["BSMITH_PROJECT_DIR"] = str(self.config.project_dir)

    return env_dict

to_env_dict

to_env_dict(include_internal: bool = True) -> dict[str, str]

Generate complete environment for shell script generation.

Includes PATH, LD_LIBRARY_PATH, and all configuration variables. Used by generate_activation_script() and generate_direnv_file().

Parameters:

Name Type Description Default
include_internal bool

Include internal BSMITH_* variables (default: True)

True

Returns:

Type Description
dict[str, str]

Dict of environment variable names to string values

Source code in brainsmith/settings/env_export.py
def to_env_dict(self, include_internal: bool = True) -> dict[str, str]:
    """Generate complete environment for shell script generation.

    Includes PATH, LD_LIBRARY_PATH, and all configuration variables.
    Used by generate_activation_script() and generate_direnv_file().

    Args:
        include_internal: Include internal BSMITH_* variables (default: True)

    Returns:
        Dict of environment variable names to string values
    """
    if include_internal:
        env_dict = self.to_all_dict()
    else:
        env_dict = self.to_external_dict()

    path_components = os.environ.get("PATH", "").split(":")

    new_paths = [
        str(p) for p in self._collect_path_additions() if str(p) not in path_components
    ]

    env_dict["PATH"] = ":".join(path_components + new_paths)

    # FINN XSI no longer requires PYTHONPATH manipulation
    # The new finn.xsi module handles path management internally

    ld_lib_components = os.environ.get("LD_LIBRARY_PATH", "").split(":")

    if self.config.vivado_path and Path(_LIBUDEV_PATH).exists():
        env_dict["LD_PRELOAD"] = _LIBUDEV_PATH

    if self.config.vivado_path:
        arch = platform.machine()
        if arch != "x86_64":
            raise RuntimeError(
                f"Brainsmith currently only supports x86_64 architecture.\n"
                f"Detected architecture: {arch}\n"
                f"Vivado integration has not been tested on this platform.\n"
                f"If you need ARM support, please open an issue."
            )

        # Add system library path (avoid duplicates)
        libc_lib = "/lib/x86_64-linux-gnu/"
        if libc_lib not in ld_lib_components:
            ld_lib_components.append(libc_lib)

        # Add Vivado library path (avoid duplicates)
        vivado_lib = str(self.config.vivado_path / "lib" / "lnx64.o")
        if vivado_lib not in ld_lib_components:
            ld_lib_components.append(vivado_lib)

    if self.config.vitis_path:
        vitis_fpo_lib = str(self.config.vitis_path / "lnx64" / "tools" / "fpo_v7_1")
        if vitis_fpo_lib not in ld_lib_components:
            ld_lib_components.append(vitis_fpo_lib)

    env_dict["LD_LIBRARY_PATH"] = ":".join(filter(None, ld_lib_components))

    # The actual HOME override is handled at container level in entrypoint scripts
    if self.config.vivado_path:
        # Ensure XILINX_LOCAL_USER_DATA is set to prevent network operations
        env_dict["XILINX_LOCAL_USER_DATA"] = "no"

    return env_dict

to_external_dict

to_external_dict() -> dict[str, str]

Generate dict of external tool environment variables.

Returns variables for external tools (FINN, Xilinx, etc.). Excludes internal BSMITH_* variables.

Returns:

Type Description
dict[str, str]

Dict of environment variable names to string values

Source code in brainsmith/settings/env_export.py
def to_external_dict(self) -> dict[str, str]:
    """Generate dict of external tool environment variables.

    Returns variables for external tools (FINN, Xilinx, etc.).
    Excludes internal BSMITH_* variables.

    Returns:
        Dict of environment variable names to string values
    """
    env = {}
    cfg = self.config

    # Xilinx tool paths (dual naming for FINN compatibility)
    # Both XILINX_* and *_PATH variants are exported for maximum FINN compatibility.
    # - XILINX_* variants: Used by FINN's Python runtime and internal scripts
    # - *_PATH variants: Used by FINN's TCL scripts during Vivado/Vitis integration
    if cfg.vivado_path:
        vivado_str = str(cfg.vivado_path)
        env["XILINX_VIVADO"] = vivado_str
        env["VIVADO_PATH"] = vivado_str

        if cfg.vivado_ip_cache:
            env["VIVADO_IP_CACHE"] = str(cfg.vivado_ip_cache)

    if cfg.vitis_path:
        vitis_str = str(cfg.vitis_path)
        env["XILINX_VITIS"] = vitis_str
        env["VITIS_PATH"] = vitis_str

    if cfg.vitis_hls_path:
        hls_str = str(cfg.vitis_hls_path)
        env["XILINX_HLS"] = hls_str
        env["HLS_PATH"] = hls_str

    env["PLATFORM_REPO_PATHS"] = cfg.vendor_platform_paths
    env["OHMYXILINX"] = str(cfg.deps_dir / "oh-my-xilinx")

    env["NETRON_PORT"] = str(cfg.netron_port)

    if cfg.finn_root:
        env["FINN_ROOT"] = str(cfg.finn_root)
    if cfg.finn_build_dir:
        env["FINN_BUILD_DIR"] = str(cfg.finn_build_dir)
    if cfg.finn_deps_dir:
        env["FINN_DEPS_DIR"] = str(cfg.finn_deps_dir)

    if cfg.default_workers:
        env["NUM_DEFAULT_WORKERS"] = str(cfg.default_workers)

    return env

Example:

from brainsmith.settings import get_config, EnvironmentExporter

config = get_config()
exporter = EnvironmentExporter(config)

# Export environment variables for external tools
env_dict = exporter.to_external_dict()
print(env_dict['FINN_ROOT'])
print(env_dict['VIVADO_PATH'])

See Also