From 0cf07dee8113d8f685708bc709875bf281bf0e50 Mon Sep 17 00:00:00 2001 From: Kaviraj Jagadeesan Date: Sun, 1 Mar 2026 15:21:08 +0530 Subject: [PATCH] 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. --- Makefile | 9 +- build.yaml.example | 17 ++ .../internal/gateway/channels_imports.go | 20 ++ cmd/picoclaw/internal/gateway/helpers.go | 14 +- docs/BUILD_CONFIG.md | 45 ++++ scripts/genbuild/main.go | 200 ++++++++++++++++++ 6 files changed, 290 insertions(+), 15 deletions(-) create mode 100644 build.yaml.example create mode 100644 cmd/picoclaw/internal/gateway/channels_imports.go create mode 100644 docs/BUILD_CONFIG.md create mode 100644 scripts/genbuild/main.go diff --git a/Makefile b/Makefile index c7375a544..ec2242173 100644 --- a/Makefile +++ b/Makefile @@ -72,8 +72,13 @@ BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH) # Default target all: build -## generate: Run generate -generate: +## genbuild: Generate channel imports from build.yaml (channels.include). Run before build to shrink binary. +genbuild: + @echo "Generating channel imports from build.yaml..." + @$(GO) run ./scripts/genbuild + +## generate: Run genbuild and go generate +generate: genbuild @echo "Run generate..." @rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true @$(GO) generate ./... diff --git a/build.yaml.example b/build.yaml.example new file mode 100644 index 000000000..5b77cdec0 --- /dev/null +++ b/build.yaml.example @@ -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] diff --git a/cmd/picoclaw/internal/gateway/channels_imports.go b/cmd/picoclaw/internal/gateway/channels_imports.go new file mode 100644 index 000000000..f8fffc23a --- /dev/null +++ b/cmd/picoclaw/internal/gateway/channels_imports.go @@ -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" +) diff --git a/cmd/picoclaw/internal/gateway/helpers.go b/cmd/picoclaw/internal/gateway/helpers.go index 747f7d44e..033765df5 100644 --- a/cmd/picoclaw/internal/gateway/helpers.go +++ b/cmd/picoclaw/internal/gateway/helpers.go @@ -13,19 +13,7 @@ import ( "github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" - _ "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" + // Channel imports are in channels_imports.go (generated by scripts/genbuild from build.yaml). "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/cron" "github.com/sipeed/picoclaw/pkg/devices" diff --git a/docs/BUILD_CONFIG.md b/docs/BUILD_CONFIG.md new file mode 100644 index 000000000..f66fbf7e3 --- /dev/null +++ b/docs/BUILD_CONFIG.md @@ -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//` and, if desired, list `` in `channels.include`. + + + +Optional denylist options also work: + +- `channels.skip: [telegram, whatsapp]` + +If `channels.include` is set, it **wins** over any skip settings. diff --git a/scripts/genbuild/main.go b/scripts/genbuild/main.go new file mode 100644 index 000000000..e84102e1e --- /dev/null +++ b/scripts/genbuild/main.go @@ -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() +}