2
0

feat: implement plugin loading system
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Failing after 4m23s
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled

- Add modules/plugins/loader.go to load .so plugins from plugins directory
- Add modules/setting/plugins.go for [plugins] configuration section
- Wire up plugin loading before DB init so plugins can register models
- Wire up plugin migrations and initialization after DB init
- Register plugin routes in web.go

Plugins can now be loaded at runtime by placing .so files in the plugins
directory and enabling [plugins] ENABLED = true in app.ini.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 10:16:42 -05:00
parent c8f591902c
commit b91b9c5711
7 changed files with 168 additions and 10 deletions

107
modules/plugins/loader.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package plugins
import (
"fmt"
"os"
"path/filepath"
"plugin"
"code.gitcaddy.com/server/modules/log"
"code.gitcaddy.com/server/modules/setting"
)
// LoadAll loads all plugins from the configured plugin directory
func LoadAll() error {
if !setting.Plugins.Enabled {
log.Info("Plugin system is disabled")
return nil
}
pluginDir := setting.Plugins.Path
if pluginDir == "" {
pluginDir = filepath.Join(setting.AppDataPath, "plugins")
}
// Check if plugin directory exists
info, err := os.Stat(pluginDir)
if os.IsNotExist(err) {
log.Info("Plugin directory does not exist: %s", pluginDir)
return nil
}
if err != nil {
return fmt.Errorf("failed to stat plugin directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("plugin path is not a directory: %s", pluginDir)
}
log.Info("Loading plugins from: %s", pluginDir)
// Find all .so files in the plugin directory
entries, err := os.ReadDir(pluginDir)
if err != nil {
return fmt.Errorf("failed to read plugin directory: %w", err)
}
loadedCount := 0
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if filepath.Ext(name) != ".so" {
continue
}
pluginPath := filepath.Join(pluginDir, name)
if err := loadPlugin(pluginPath); err != nil {
log.Error("Failed to load plugin %s: %v", name, err)
// Continue loading other plugins
continue
}
loadedCount++
}
log.Info("Loaded %d plugin(s)", loadedCount)
return nil
}
// loadPlugin loads a single plugin from a .so file
func loadPlugin(path string) error {
log.Info("Loading plugin: %s", path)
// Open the plugin
p, err := plugin.Open(path)
if err != nil {
return fmt.Errorf("failed to open plugin: %w", err)
}
// Look for the Plugin symbol
sym, err := p.Lookup("Plugin")
if err != nil {
return fmt.Errorf("plugin does not export 'Plugin' symbol: %w", err)
}
// The Plugin symbol should be a pointer to a Plugin interface implementation
plug, ok := sym.(*Plugin)
if !ok {
// Try to see if it's the interface directly
plugDirect, ok := sym.(Plugin)
if !ok {
return fmt.Errorf("Plugin symbol is not a plugins.Plugin (got %T)", sym)
}
Register(plugDirect)
return nil
}
if plug == nil || *plug == nil {
return fmt.Errorf("Plugin symbol is nil")
}
Register(*plug)
return nil
}

View File

@@ -6,7 +6,6 @@ package plugins
import (
"context"
"github.com/go-chi/chi/v5"
"xorm.io/xorm"
)
@@ -44,7 +43,8 @@ type WebRoutesPlugin interface {
Plugin
// RegisterWebRoutes adds routes to the web UI router
RegisterWebRoutes(m chi.Router)
// The router is a *web.Router from the gitcaddy server
RegisterWebRoutes(m any)
}
// APIRoutesPlugin is implemented by plugins that add API routes
@@ -52,7 +52,8 @@ type APIRoutesPlugin interface {
Plugin
// RegisterAPIRoutes adds routes to the API router
RegisterAPIRoutes(m chi.Router)
// The router is a *web.Router from the gitcaddy server
RegisterAPIRoutes(m any)
}
// RepoRoutesPlugin is implemented by plugins that add per-repository routes
@@ -60,10 +61,12 @@ type RepoRoutesPlugin interface {
Plugin
// RegisterRepoWebRoutes adds routes under /{owner}/{repo}/
RegisterRepoWebRoutes(m chi.Router)
// The router is a *web.Router from the gitcaddy server
RegisterRepoWebRoutes(m any)
// RegisterRepoAPIRoutes adds routes under /api/v1/repos/{owner}/{repo}/
RegisterRepoAPIRoutes(m chi.Router)
// The router is a *web.Router from the gitcaddy server
RegisterRepoAPIRoutes(m any)
}
// LicensedPlugin is implemented by plugins that require a license

View File

@@ -9,7 +9,6 @@ import (
"code.gitcaddy.com/server/modules/log"
"github.com/go-chi/chi/v5"
"xorm.io/xorm"
)
@@ -126,7 +125,7 @@ func MigrateAll(ctx context.Context, x *xorm.Engine) error {
}
// RegisterWebRoutes registers web UI routes from all plugins
func RegisterWebRoutes(m chi.Router) {
func RegisterWebRoutes(m any) {
registryLock.RLock()
defer registryLock.RUnlock()
@@ -139,7 +138,7 @@ func RegisterWebRoutes(m chi.Router) {
}
// RegisterAPIRoutes registers API routes from all plugins
func RegisterAPIRoutes(m chi.Router) {
func RegisterAPIRoutes(m any) {
registryLock.RLock()
defer registryLock.RUnlock()
@@ -152,7 +151,7 @@ func RegisterAPIRoutes(m chi.Router) {
}
// RegisterRepoWebRoutes registers per-repository web routes from all plugins
func RegisterRepoWebRoutes(m chi.Router) {
func RegisterRepoWebRoutes(m any) {
registryLock.RLock()
defer registryLock.RUnlock()
@@ -165,7 +164,7 @@ func RegisterRepoWebRoutes(m chi.Router) {
}
// RegisterRepoAPIRoutes registers per-repository API routes from all plugins
func RegisterRepoAPIRoutes(m chi.Router) {
func RegisterRepoAPIRoutes(m any) {
registryLock.RLock()
defer registryLock.RUnlock()

View File

@@ -0,0 +1,21 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
// Plugins settings
var (
Plugins = struct {
Enabled bool
Path string
}{
Enabled: false, // Disabled by default
Path: "", // Empty means default to APP_DATA_PATH/plugins
}
)
func loadPluginsFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("plugins")
Plugins.Enabled = sec.Key("ENABLED").MustBool(false)
Plugins.Path = sec.Key("PATH").MustString("")
}

View File

@@ -150,6 +150,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadMarkupFrom(cfg)
loadGlobalLockFrom(cfg)
loadOtherFrom(cfg)
loadPluginsFrom(cfg)
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"code.gitcaddy.com/server/models"
ai_model "code.gitcaddy.com/server/models/ai"
authmodel "code.gitcaddy.com/server/models/auth"
"code.gitcaddy.com/server/models/db"
"code.gitcaddy.com/server/modules/cache"
"code.gitcaddy.com/server/modules/eventsource"
"code.gitcaddy.com/server/modules/git"
@@ -20,6 +21,7 @@ import (
"code.gitcaddy.com/server/modules/log"
"code.gitcaddy.com/server/modules/markup"
"code.gitcaddy.com/server/modules/markup/external"
"code.gitcaddy.com/server/modules/plugins"
"code.gitcaddy.com/server/modules/setting"
"code.gitcaddy.com/server/modules/ssh"
"code.gitcaddy.com/server/modules/storage"
@@ -58,6 +60,8 @@ import (
"code.gitcaddy.com/server/services/task"
"code.gitcaddy.com/server/services/uinotification"
"code.gitcaddy.com/server/services/webhook"
"xorm.io/xorm"
)
func mustInit(fn func() error) {
@@ -144,8 +148,22 @@ func InitWebInstalled(ctx context.Context) {
log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.")
}
// Load plugins before DB init so they can register their models
mustInit(plugins.LoadAll)
mustInitCtx(ctx, common.InitDBEngine)
log.Info("ORM engine initialization successful!")
// Run plugin migrations after DB init
if err := plugins.MigrateAll(ctx, db.GetEngine(ctx).(*xorm.Engine)); err != nil {
log.Fatal("Plugin migrations failed: %v", err)
}
// Initialize all plugins
if err := plugins.InitAll(ctx); err != nil {
log.Fatal("Plugin initialization failed: %v", err)
}
mustInit(system.Init)
mustInitCtx(ctx, oauth2.Init)
mustInitCtx(ctx, oauth2_provider.Init)

View File

@@ -15,6 +15,7 @@ import (
"code.gitcaddy.com/server/modules/graceful"
"code.gitcaddy.com/server/modules/log"
"code.gitcaddy.com/server/modules/metrics"
"code.gitcaddy.com/server/modules/plugins"
"code.gitcaddy.com/server/modules/public"
"code.gitcaddy.com/server/modules/setting"
"code.gitcaddy.com/server/modules/storage"
@@ -1799,6 +1800,14 @@ func registerWebRoutes(m *web.Router) {
})
}
// Register plugin web routes
plugins.RegisterWebRoutes(m)
// Register plugin repo routes (mounted under /{username}/{reponame})
m.Group("/{username}/{reponame}", func() {
plugins.RegisterRepoWebRoutes(m)
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty)
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetWebContext(req.Context())
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()