From 37473a7096e2deaff398e622b15d8cb2ac716045 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 30 Dec 2025 21:59:03 -0500 Subject: [PATCH] fix(plugins): generate host wrappers Signed-off-by: Deluan --- plugins/cmd/ndpgen/main.go | 82 ++++++++++++++++++++++++++++++++++++-- plugins/host/doc.go | 1 + 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/plugins/cmd/ndpgen/main.go b/plugins/cmd/ndpgen/main.go index ddb0a5ea0..b1dd27cc5 100644 --- a/plugins/cmd/ndpgen/main.go +++ b/plugins/cmd/ndpgen/main.go @@ -5,7 +5,10 @@ // // Usage: // -// # Generate host wrappers (from plugins/host to plugins/pdk) +// # Generate host wrappers for Navidrome server (output to input directory) +// ndpgen -host-wrappers -input=./plugins/host -package=host +// +// # Generate PDK client wrappers (from plugins/host to plugins/pdk) // ndpgen -host-only -input=./plugins/host -output=./plugins/pdk // // # Generate capability wrappers (from plugins/capabilities to plugins/pdk) @@ -15,6 +18,7 @@ // ndpgen -schemas -input=./plugins/capabilities // // Output directories: +// - Host wrappers: $input/_gen.go (server-side, used by Navidrome) // - Host functions: $output/go/host/, $output/python/host/, $output/rust/host/ // - Capabilities: $output/go// (e.g., $output/go/metadata/) // - Schemas: $input/.yaml (co-located with Go sources) @@ -24,7 +28,8 @@ // -input Input directory containing Go source files with annotated interfaces // -output Output directory base for generated files (default: same as input) // -package Output package name for Go (default: host for host-only, auto for capabilities) -// -host-only Generate only host function wrappers +// -host-wrappers Generate server-side host wrappers (used by Navidrome, output to input directory) +// -host-only Generate PDK client wrappers for calling host functions // -capability-only Generate only capability export wrappers // -schemas Generate XTP YAML schemas from capabilities // -go Generate Go client wrappers (default: true when not using -python/-rust) @@ -54,6 +59,7 @@ type config struct { rustOutputDir string // Rust output: $outputDir/rust/host pkgName string hostOnly bool + hostWrappers bool // Generate host wrappers (used by Navidrome server) capabilityOnly bool schemasOnly bool // Generate XTP schemas from capabilities (output goes to inputDir) generateGoClient bool @@ -86,6 +92,14 @@ func main() { return } + if cfg.hostWrappers { + if err := runHostWrapperGeneration(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return + } + // Default: host-only mode services, err := parseServices(cfg) if err != nil { @@ -134,6 +148,31 @@ func runSchemaGeneration(cfg *config) error { return generateSchemas(cfg, capabilities) } +// runHostWrapperGeneration handles host wrapper code generation. +// This generates the *_gen.go files in the input directory that are used +// by Navidrome server to expose host functions to plugins. +func runHostWrapperGeneration(cfg *config) error { + services, err := parseServices(cfg) + if err != nil { + return err + } + if len(services) == 0 { + if cfg.verbose { + fmt.Println("No host services found") + } + return nil + } + + // Generate host wrappers for each service + for _, svc := range services { + if err := generateHostWrapperCode(svc, cfg.inputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil { + return fmt.Errorf("generating host wrapper for %s: %w", svc.Name, err) + } + } + + return nil +} + // parseConfig parses command-line flags and returns the configuration. func parseConfig() (*config, error) { var ( @@ -141,6 +180,7 @@ func parseConfig() (*config, error) { outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)") pkgName = flag.String("package", "", "Output package name for Go (default: host for host-only, auto for capabilities)") hostOnly = flag.Bool("host-only", false, "Generate only host function wrappers") + hostWrappers = flag.Bool("host-wrappers", false, "Generate host wrappers (used by Navidrome server, output to input directory)") capabilityOnly = flag.Bool("capability-only", false, "Generate only capability export wrappers") schemasOnly = flag.Bool("schemas", false, "Generate XTP YAML schemas from capabilities (output to input directory)") goClient = flag.Bool("go", false, "Generate Go client wrappers") @@ -156,6 +196,9 @@ func parseConfig() (*config, error) { if *hostOnly { modeCount++ } + if *hostWrappers { + modeCount++ + } if *capabilityOnly { modeCount++ } @@ -170,7 +213,7 @@ func parseConfig() (*config, error) { // Cannot specify multiple modes if modeCount > 1 { - return nil, fmt.Errorf("cannot specify multiple modes (-host-only, -capability-only, -schemas)") + return nil, fmt.Errorf("cannot specify multiple modes (-host-only, -host-wrappers, -capability-only, -schemas)") } if *outputDir == "" { @@ -214,6 +257,7 @@ func parseConfig() (*config, error) { rustOutputDir: absRustOutput, pkgName: *pkgName, hostOnly: *hostOnly, + hostWrappers: *hostWrappers, capabilityOnly: *capabilityOnly, schemasOnly: *schemasOnly, generateGoClient: *goClient || !anyLangFlag, @@ -469,6 +513,38 @@ func generateAllCode(cfg *config, services []internal.Service) error { return nil } +// generateHostWrapperCode generates host wrapper code for a service. +// This generates the *_gen.go files that are used by Navidrome server +// to expose host functions to plugins via Extism. +func generateHostWrapperCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error { + code, err := internal.GenerateHost(svc, pkgName) + if err != nil { + return fmt.Errorf("generating code: %w", err) + } + + formatted, err := format.Source(code) + if err != nil { + return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code) + } + + // Host wrapper file follows the pattern _gen.go + hostFile := filepath.Join(outputDir, strings.ToLower(svc.Name)+"_gen.go") + + if dryRun { + fmt.Printf("=== %s ===\n%s\n", hostFile, formatted) + } else { + if err := os.WriteFile(hostFile, formatted, 0600); err != nil { + return fmt.Errorf("writing file: %w", err) + } + + if verbose { + fmt.Printf("Generated host wrapper: %s\n", hostFile) + } + } + + return nil +} + // generateGoClientCode generates Go client-side code for a service. func generateGoClientCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error { code, err := internal.GenerateClientGo(svc, pkgName) diff --git a/plugins/host/doc.go b/plugins/host/doc.go index d1880f571..ec64c65a8 100644 --- a/plugins/host/doc.go +++ b/plugins/host/doc.go @@ -37,6 +37,7 @@ // Generated files follow the pattern _gen.go and include a header comment // indicating they should not be edited manually. // +//go:generate go run ../cmd/ndpgen -host-wrappers -input=. -package=host //go:generate go run ../cmd/ndpgen -input=. -output=../pdk -go -python -rust //go:generate go mod tidy -C ../pdk/go package host