feat(build): introduce build configuration for channel inclusion

- Add `build.yaml.example` for configuring which channels to compile into the binary.
- Implement `genbuild` script to generate channel imports based on `build.yaml`.
- Update `Makefile` to run `genbuild` automatically during the build process.
- Create documentation in `BUILD_CONFIG.md` to guide users on configuring and using the build system.
- Move channel imports to `channels_imports.go`, generated by `genbuild`, to reduce binary size.
This commit is contained in:
Kaviraj Jagadeesan 2026-03-01 15:21:08 +05:30
parent cadcdc0b41
commit 0cf07dee81
6 changed files with 290 additions and 15 deletions

View file

@ -72,8 +72,13 @@ BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH)
# Default target # Default target
all: build all: build
## generate: Run generate ## genbuild: Generate channel imports from build.yaml (channels.include). Run before build to shrink binary.
generate: genbuild:
@echo "Generating channel imports from build.yaml..."
@$(GO) run ./scripts/genbuild
## generate: Run genbuild and go generate
generate: genbuild
@echo "Run generate..." @echo "Run generate..."
@rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true @rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true
@$(GO) generate ./... @$(GO) generate ./...

17
build.yaml.example Normal file
View file

@ -0,0 +1,17 @@
# Build configuration: control which channels (and optionally providers/tools in the future)
# are compiled into the binary to reduce size. Copy to build.yaml and edit. Used when you run:
# make build
# (or run: go run ./scripts/genbuild before go build).
#
# See docs/BUILD_CONFIG.md.
channels:
# Preferred allowlist model: list the channel names you want to include.
# If empty or omitted, all discovered channels under pkg/channels/ are included.
#
# Channel names must match the channel package directory under pkg/channels/.
include: []
# include: [telegram, whatsapp, discord, slack, feishu, dingtalk, wecom, qq, line, onebot, maixcam, pico, whatsapp_native]
# Optional denylist model:
# skip: [telegram, whatsapp]

View file

@ -0,0 +1,20 @@
// Code generated by scripts/genbuild. DO NOT EDIT.
// Edit build.yaml and run: go run ./scripts/genbuild (or make build).
package gateway
import (
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
_ "github.com/sipeed/picoclaw/pkg/channels/line"
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
_ "github.com/sipeed/picoclaw/pkg/channels/onebot"
_ "github.com/sipeed/picoclaw/pkg/channels/pico"
_ "github.com/sipeed/picoclaw/pkg/channels/qq"
_ "github.com/sipeed/picoclaw/pkg/channels/slack"
_ "github.com/sipeed/picoclaw/pkg/channels/telegram"
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native"
)

View file

@ -13,19 +13,7 @@ import (
"github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk" // Channel imports are in channels_imports.go (generated by scripts/genbuild from build.yaml).
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
_ "github.com/sipeed/picoclaw/pkg/channels/line"
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
_ "github.com/sipeed/picoclaw/pkg/channels/onebot"
_ "github.com/sipeed/picoclaw/pkg/channels/pico"
_ "github.com/sipeed/picoclaw/pkg/channels/qq"
_ "github.com/sipeed/picoclaw/pkg/channels/slack"
_ "github.com/sipeed/picoclaw/pkg/channels/telegram"
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron" "github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices" "github.com/sipeed/picoclaw/pkg/devices"

45
docs/BUILD_CONFIG.md Normal file
View file

@ -0,0 +1,45 @@
## Build configuration (optional smaller binary)
You can control **which channels are compiled into the binary** to reduce size. This is done **before** the build by parsing a config file and generating which channel packages get compiled in.
### How it works
1. **Config**: Create `build.yaml` in the repo root (copy from `build.yaml.example`).
2. **Allowlist**: Set `channels.include` to the channel names you want to compile in (e.g. `[telegram, whatsapp]`).
3. **Generate**: Run the generator so it writes `cmd/picoclaw/internal/gateway/channels_imports.go` with only the selected channel imports.
4. **Build**: Run `make build`. The generator is run automatically as part of `make build`.
If there is no `build.yaml`, or `channels.include` is empty / omitted, **all discovered channels** under `pkg/channels/` are included by default.
### Quick start
```bash
# Copy example and edit (list channels to include)
cp build.yaml.example build.yaml
# Edit build.yaml, e.g.:
# channels:
# include: [telegram, whatsapp]
# Build (generate runs first and overwrites channel imports)
make build
```
Or without Make:
```bash
go run ./scripts/genbuild # generate channel imports from build.yaml
go build -o picoclaw ./cmd/picoclaw
```
### Channel names
Channel names in `channels.include` must match the directory name under `pkg/channels/`. The generator **automatically discovers** all channel packages by reading the `pkg/channels/` directory, so you usually don't need to touch any code when adding a new channel—just create `pkg/channels/<name>/` and, if desired, list `<name>` in `channels.include`.
Optional denylist options also work:
- `channels.skip: [telegram, whatsapp]`
If `channels.include` is set, it **wins** over any skip settings.

200
scripts/genbuild/main.go Normal file
View file

@ -0,0 +1,200 @@
// genbuild reads build.yaml (channels.include) and
// generates cmd/picoclaw/internal/gateway/channels_imports.go so only the
// selected channels are compiled in, reducing binary size.
//
// Run before build: go run ./scripts/genbuild
// Or use: make build (which runs this via the generate target).
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
const (
buildConfigName = "build.yaml"
outputPath = "cmd/picoclaw/internal/gateway/channels_imports.go"
modulePath = "github.com/sipeed/picoclaw"
)
type buildConfig struct {
// Preferred allowlist model:
// channels:
// include: [telegram, whatsapp]
// Optional denylist model:
// channels:
// skip: [telegram, whatsapp]
Channels struct {
Include []string `yaml:"include"`
Skip []string `yaml:"skip"`
} `yaml:"channels"`
}
func main() {
repoRoot, err := findRepoRoot()
if err != nil {
fmt.Fprintf(os.Stderr, "genbuild: %v\n", err)
os.Exit(1)
}
cfg := loadConfig(repoRoot)
allChannels, err := discoverChannels(repoRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "genbuild: discover channels: %v\n", err)
os.Exit(1)
}
include := selectChannels(allChannels, cfg)
outPath := filepath.Join(repoRoot, outputPath)
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "genbuild: %v\n", err)
os.Exit(1)
}
content := generateImports(include)
if err := os.WriteFile(outPath, []byte(content), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "genbuild: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "genbuild: wrote %s (%d channels)\n", outputPath, len(include))
}
func findRepoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", fmt.Errorf("go.mod not found (run from repo root)")
}
dir = parent
}
}
func loadConfig(repoRoot string) *buildConfig {
cfg := &buildConfig{}
path := filepath.Join(repoRoot, buildConfigName)
data, err := os.ReadFile(path)
if err != nil {
// No build.yaml: include all channels
return cfg
}
if err := yaml.Unmarshal(data, cfg); err != nil {
fmt.Fprintf(os.Stderr, "genbuild: warning: %s: %v (including all channels)\n", path, err)
return cfg
}
return cfg
}
// discoverChannels returns all channel package names (directory names under pkg/channels/).
func discoverChannels(repoRoot string) ([]string, error) {
dir := filepath.Join(repoRoot, "pkg", "channels")
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var channels []string
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
continue
}
channels = append(channels, name)
}
sort.Strings(channels)
return channels, nil
}
// selectChannels applies the config to the discovered channels, preferring the
// allowlist model (channels.include), and optionally the denylist model (channels.skip).
func selectChannels(all []string, cfg *buildConfig) []string {
if len(all) == 0 {
return nil
}
available := make(map[string]struct{}, len(all))
for _, ch := range all {
available[ch] = struct{}{}
}
// 1) Preferred allowlist: channels.include
if len(cfg.Channels.Include) > 0 {
var include []string
for _, raw := range cfg.Channels.Include {
name := strings.TrimSpace(raw)
if name == "" {
continue
}
if _, ok := available[name]; !ok {
fmt.Fprintf(os.Stderr, "genbuild: warning: channel %q listed in channels.include but no such pkg/channels/%s\n", name, name)
continue
}
include = append(include, name)
}
if len(include) == 0 {
// Nothing matched; fall back to "all" to avoid surprising empty builds.
return all
}
sort.Strings(include)
return include
}
// 2) denylist: channels.skip.
skipSet := make(map[string]struct{})
for _, raw := range cfg.Channels.Skip {
name := strings.TrimSpace(raw)
if name == "" {
continue
}
skipSet[name] = struct{}{}
}
if len(skipSet) == 0 {
// No config at all or no skip entries: include all channels.
return all
}
var include []string
for _, ch := range all {
if _, skip := skipSet[ch]; skip {
continue
}
include = append(include, ch)
}
return include
}
func generateImports(channels []string) string {
var b strings.Builder
b.WriteString("// Code generated by scripts/genbuild. DO NOT EDIT.\n")
b.WriteString("// Edit build.yaml and run: go run ./scripts/genbuild (or make build).\n\n")
b.WriteString("package gateway\n\n")
b.WriteString("import (\n")
for _, ch := range channels {
b.WriteString("\t_ \"")
b.WriteString(modulePath)
b.WriteString("/pkg/channels/")
b.WriteString(ch)
b.WriteString("\"\n")
}
b.WriteString(")\n")
return b.String()
}