PSPublishModule already generates production module bootstrappers during staging and packaging. Development bootstrappers are different: they live in the source checkout and make local F5/import workflows easier while the final packaged module remains production-shaped.
Use this feature when a source checkout needs to import a freshly built local .NET binary before the module is packaged. It is intentionally a development-time convenience, not a replacement for the packaged bootstrapper.
A scan across the Evotec repository root found three useful families:
- Script-only modules that do not need binary development loading.
- Simple binary modules with older always-on development
.psm1loaders. - A small set of newer or conflict-prone binary modules, such as PSWriteOffice, that need environment-gated local binary loading and sometimes AssemblyLoadContext support on PowerShell Core.
That does not justify many per-module templates. The shared scope is intentionally small:
Off: do not generate development binary loading logic.Environment: generate development binary loading logic, but only activate it when the module-specific environment variable istrue.Auto: generate development binary loading logic and use it automatically when a matching local build output exists.
Environment is the default when New-ConfigurationBuild -NETDevelopmentBinaries is used. This keeps local source bin outputs from taking over a module import unless the maintainer opts in for that shell.
Use development bootstrappers for:
- A binary-backed module where maintainers run
dotnet buildor F5 from an IDE and then import the source module without packaging it first. - A module with dependency conflicts that should use
NETAssemblyLoadContexton PowerShell Core during local development, matching the packaged module behavior more closely. - A source tree with checked-in
Public,Private,Classes,Enums, orLiblayout where PowerForge can regenerate the source.psm1safely. - A team repository where the source
.psm1should include the loader, but local binaries should only win when a maintainer explicitly sets an environment variable.
Avoid development bootstrappers for:
- Pure script modules with no local binary output.
- Hand-authored single-file
.psm1modules unless you explicitly chooseReplaceSingleFile. - Source layouts that depend on custom
Information.IncludePS1folders not represented by the standard source loader. PowerForge preserves those files by default so custom dot-sourcing is not lost. - CI/package validation. The packaged/staged bootstrapper remains the proof for release behavior.
The main entrypoint is still New-ConfigurationBuild / Invoke-ModuleBuild.
New-ConfigurationBuild `
-NETProjectPath "$PSScriptRoot\..\Sources\Example\Example.csproj" `
-NETProjectName 'Example' `
-NETFramework 'net8.0', 'net472' `
-NETAssemblyLoadContext `
-NETDevelopmentBinariesOptional overrides exist for unusual layouts:
-NETDevelopmentBinariesMode Environment|Auto|Off-NETDevelopmentBinariesPath <path-to-bin-root>-NETDevelopmentBinariesEnvironmentVariable <name>-NETDevelopmentConfigurationEnvironmentVariable <name>-NETDevelopmentSourceBootstrapperMode PreserveSingleFile|ReplaceSingleFile
When no environment names are supplied, PowerForge derives:
<MODULE>_USE_DEVELOPMENT_BINARIES<MODULE>_DEVELOPMENT_CONFIGURATION
The generated source .psm1 probes bin\<Configuration>\<TFM>\<Module>.dll, preferring Core-compatible TFMs in PowerShell Core and desktop-compatible TFMs in Windows PowerShell. PowerShell Core uses the generated development ALC loader when NETAssemblyLoadContext is enabled; Windows PowerShell keeps the direct-import compatibility path.
Modules that intentionally exchange types from a common dependency can declare exact assembly simple names with New-ConfigurationBuild -NETSharedAssemblies 'Common.Runtime', 'Common.Display'. SharedAssemblies is an alias. The corresponding build-spec and BuildLibraries JSON property is SharedAssemblies.
Use -NETPreloadSharedAssemblies 'Common.Runtime' (alias PreloadSharedAssemblies, with the same JSON property name) for the subset that must load before the binary data host initializes. Declaration alone does not preload a dependency, export its commands, or scan its types. Shared dependencies are excluded from the generated blanket library preload. Empty lists preserve the existing loading policy.
On PowerShell Core, the module's private ALC checks declared shared names before its dependency resolvers and adjacent files. A single process policy reuses an already loaded copy before looking in the selected module payload. The first loaded shared dependency establishes the non-collectible shared context; a fresh process uses Default. This also allows an older dependency loaded by an earlier private loader to remain the shared runtime. Other dependencies stay private. Already duplicated shared names, collectible contexts, or shared dependencies spread across different contexts are rejected rather than creating another runtime. Windows PowerShell reuses the first loaded AppDomain copy. Removing a module does not unload or replace shared assemblies; start a new process to change versions.
Sharing requires a compatible assembly identity and ABI for the process lifetime. Keep shared dependencies out of NETBinaryModule; binary exports must remain product data hosts. Names cannot contain paths, filename extensions, wildcards or display-name qualifiers, and system/PowerShell host assemblies cannot be opted in. PreloadSharedAssemblies must be a subset of SharedAssemblies.
Nonempty shared policies give packaged and development loader helpers distinct assembly and type identities, based on the policy capability and a deterministic, case-insensitive name-set hash. Importing a newer module from a different physical version directory therefore cannot reuse an older helper that lacks its shared policy. Empty policies retain the historical helper identities.
The process policy registers exact shared names and their selected payload paths without loading them. When a module first requests a lazy shared dependency, its own registered payload supplies that dependency; an earlier declaration alone cannot select another module's unloaded copy. Once loaded, the process reuses that instance. The policy checks the requested name, culture and public key token before loading a file or returning an existing instance; a different version with the same identity can still satisfy the request. A CLR resolver attached to the chosen shared context resolves lazy shared-to-shared references from the registered map; Desktop also checks the requesting shared assembly's provenance. These callbacks last for the process lifetime, so removing the product module does not break later use of a loaded shared adapter. Empty and partial preload lists remain lazy; registration does not preload all declared dependencies.
For optional presentation, preload only the runtime needed before data-host import. Keep the optional adapter in SharedAssemblies and any private presentation assembly in NETIgnoreLibraryOnLoad. Product code reads the loaded runtime's metadata before loading those optional assemblies or calling new members. If the loaded feature level is insufficient, the product keeps native formatting and its data cmdlets available. PowerForge does not select product feature levels or load optional presentation on a product's behalf.
Both development and packaged ALC loaders identify module DLLs by their physical paths, resolving symbolic links and Windows directory junctions. Importing the same module through a physical directory and an alias reuses its assembly and cmdlet types; aliases in an export group also identify one member. Separate physical module copies keep separate contexts. Contexts remain non-collectible: restart the process to use a replaced DLL at the same physical path.
With the default Environment mode, a maintainer opts into local binaries per shell:
$env:EXAMPLE_USE_DEVELOPMENT_BINARIES = 'true'
$env:EXAMPLE_DEVELOPMENT_CONFIGURATION = 'Debug'
Import-Module .\Example.psd1 -ForceUnset the variable, or set it to anything other than true, to go back to the packaged/source fallback:
Remove-Item Env:\EXAMPLE_USE_DEVELOPMENT_BINARIES -ErrorAction SilentlyContinue
Import-Module .\Example.psd1 -ForceUse Auto only when a repository's normal source import should always prefer a matching local build output when one exists:
New-ConfigurationBuild `
-NETProjectPath "$PSScriptRoot\..\Sources\Example\Example.csproj" `
-NETProjectName 'Example' `
-NETDevelopmentBinaries `
-NETDevelopmentBinariesMode AutoDocumentation mode leaves the source .psm1 unchanged, even when development binaries are disabled for that run. Use build mode to regenerate the source loader or clear development loading logic.
PowerForge only overwrites source .psm1 files when the layout is generated-safe:
- Standard script folders:
Classes,Enums,Private,Public. - Binary source layout: checked-in
Lib\<layout>folders. - Existing PowerForge-generated development bootstrapper files.
Hand-authored single-file source modules are preserved by default. To replace one intentionally:
New-ConfigurationBuild `
-NETProjectPath "$PSScriptRoot\..\Sources\Example\Example.csproj" `
-NETDevelopmentBinaries `
-NETDevelopmentSourceBootstrapperMode ReplaceSingleFileWhen DevelopmentBinariesMode is Off, PowerForge removes development loading logic from known generated source bootstrappers. For Lib-based source layouts it regenerates the normal source binary loader instead of deleting the module entrypoint.
Build-level JSON uses unprefixed Build properties:
{
"Build": {
"Name": "Example",
"SourcePath": "Module",
"CsprojPath": "Sources/Example/Example.csproj",
"DevelopmentBinariesMode": "Environment",
"DevelopmentBinariesPath": "Sources/Example/bin",
"DevelopmentBinariesEnvironmentVariable": "EXAMPLE_USE_DEVELOPMENT_BINARIES",
"DevelopmentConfigurationEnvironmentVariable": "EXAMPLE_DEVELOPMENT_CONFIGURATION",
"DevelopmentSourceBootstrapperMode": "PreserveSingleFile"
}
}BuildLibraries segments may use either the unprefixed names or the cmdlet-style NET* aliases:
{
"Type": "BuildLibraries",
"BuildLibraries": {
"NETProjectPath": "Sources/Example/Example.csproj",
"NETAssemblyLoadContext": true,
"NETDevelopmentBinaries": true,
"NETDevelopmentBinariesMode": "Environment",
"NETDevelopmentBinariesPath": "Sources/Example/bin"
}
}For build-level DevelopmentBinariesPath, relative paths are resolved from the config file directory, like CsprojPath. For segment-level DevelopmentBinariesPath, relative paths stay source-root relative, matching the rest of the BuildLibraries segment behavior.
Prefer fixing PowerForge/PSPublishModule when several repositories need the same development import behavior. Repository-local .psm1 loaders should be kept for truly unusual source layouts only. Common patterns that belong in PowerForge include:
- Environment-gated local binary loading.
- AssemblyLoadContext source imports on PowerShell Core.
- Direct-import reuse when a development assembly is already loaded.
- Runtime/native probing for development outputs.
- Safe source
.psm1regeneration rules for standard script folders andLiblayouts.