diff --git a/.gitignore b/.gitignore index 135867842..e1736f56b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,10 @@ dist/ # Windows Application Icon/Resource *.syso +.cache/ +web/frontend/.pnpm-store/ +_tmp_* +web/frontend/_tmp_* # Test telegram integration cmd/telegram/ diff --git a/Makefile b/Makefile index c5d691c29..acb258370 100644 --- a/Makefile +++ b/Makefile @@ -7,19 +7,43 @@ CMD_DIR=cmd/$(BINARY_NAME) MAIN_GO=$(CMD_DIR)/main.go EXT= +ifeq ($(OS),Windows_NT) + POWERSHELL=powershell -NoProfile -Command + WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) +endif + # Version -VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") -BUILD_TIME=$(shell date +%FT%T%z) -GO_VERSION=$(shell $(GO) version | awk '{print $$3}') +ifeq ($(OS),Windows_NT) + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>NUL)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>NUL)) + BUILD_TIME_RAW:=$(strip $(shell powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'")) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>NUL)) +else + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>/dev/null)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>/dev/null)) + BUILD_TIME_RAW:=$(strip $(shell date +%FT%T%z)) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>/dev/null)) +endif +VERSION?=$(if $(VERSION_RAW),$(VERSION_RAW),dev) +GIT_COMMIT=$(if $(GIT_COMMIT_RAW),$(GIT_COMMIT_RAW),dev) +BUILD_TIME=$(if $(BUILD_TIME_RAW),$(BUILD_TIME_RAW),dev) +GO_VERSION=$(if $(GO_VERSION_RAW),$(GO_VERSION_RAW),unknown) CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w # Go variables -GO?=CGO_ENABLED=0 go +GO?=go WEB_GO?=$(GO) +CGO_ENABLED?=0 GO_BUILD_TAGS?=goolm,stdjson GOFLAGS?=-v -tags $(GO_BUILD_TAGS) +GOCACHE?=$(CURDIR)/.cache/go-build +GOMODCACHE?=$(CURDIR)/.cache/go-mod +GOTOOLCHAIN?=local +export CGO_ENABLED +export GOCACHE +export GOMODCACHE +export GOTOOLCHAIN comma:=, empty:= space:=$(empty) $(empty) @@ -73,8 +97,21 @@ BUILTIN_SKILLS_DIR=$(CURDIR)/skills LNCMD=ln -sf # OS detection -UNAME_S?=$(shell uname -s) -UNAME_M?=$(shell uname -m) +ifeq ($(OS),Windows_NT) + UNAME_S=Windows + ifeq ($(WINDOWS_GOARCH_RAW),amd64) + UNAME_M=x86_64 + else ifeq ($(WINDOWS_GOARCH_RAW),arm64) + UNAME_M=arm64 + else ifeq ($(WINDOWS_GOARCH_RAW),386) + UNAME_M=x86 + else + UNAME_M=$(if $(WINDOWS_GOARCH_RAW),$(WINDOWS_GOARCH_RAW),x86_64) + endif +else + UNAME_S?=$(shell uname -s) + UNAME_M?=$(shell uname -m) +endif # Platform-specific settings ifeq ($(UNAME_S),Linux) @@ -122,6 +159,18 @@ else endif +ifeq ($(OS),Windows_NT) + PLATFORM=windows + ifeq ($(UNAME_M),x86_64) + ARCH?=amd64 + else ifeq ($(UNAME_M),arm64) + ARCH?=arm64 + else + ARCH?=$(UNAME_M) + endif + EXT=.exe +endif + BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH) # Default target @@ -130,21 +179,37 @@ all: build ## generate: Run generate generate: @echo "Run generate..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if (Test-Path -LiteralPath './$(CMD_DIR)/workspace') { Remove-Item -LiteralPath './$(CMD_DIR)/workspace' -Recurse -Force }" +else @rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true +endif @$(GO) generate ./... @echo "Run generate complete" ## build: Build the picoclaw binary for current platform build: generate @echo "Building $(BINARY_NAME)$(EXT) for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY_PATH)$(EXT) ./$(CMD_DIR) + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BINARY_PATH)$(EXT)' -Destination '$(BUILD_DIR)/$(BINARY_NAME)$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @GOARCH=${ARCH} $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY_PATH)$(EXT) ./$(CMD_DIR) @echo "Build complete: $(BINARY_PATH)$(EXT)" @$(LNCMD) $(BINARY_NAME)-$(PLATFORM)-$(ARCH)$(EXT) $(BUILD_DIR)/$(BINARY_NAME)$(EXT) +endif + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)$(EXT)" ## build-launcher: Build the picoclaw-launcher (web console) binary build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @GOARCH=${ARCH} $(MAKE) -C web build \ OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ @@ -152,6 +217,7 @@ build-launcher: GO_BUILD_TAGS='$(GO_BUILD_TAGS)' \ LDFLAGS='$(LDFLAGS)' @$(LNCMD) picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT) $(BUILD_DIR)/picoclaw-launcher$(EXT) +endif @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher$(EXT)" build-launcher-frontend: @@ -160,10 +226,16 @@ build-launcher-frontend: ## build-launcher-tui: Build the picoclaw-launcher TUI binary build-launcher-tui: @echo "Building picoclaw-launcher-tui for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH)$(EXT) ./cmd/picoclaw-launcher-tui + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher-tui$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH) ./cmd/picoclaw-launcher-tui @ln -sf picoclaw-launcher-tui-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher-tui - @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-tui" +endif + @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-tui$(EXT)" ## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary build-whatsapp-native: generate @@ -290,7 +362,11 @@ uninstall-all: ## clean: Remove build artifacts clean: @echo "Cleaning build artifacts..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if (Test-Path -LiteralPath '$(BUILD_DIR)') { Remove-Item -LiteralPath '$(BUILD_DIR)' -Recurse -Force }" +else @rm -rf $(BUILD_DIR) +endif @echo "Clean complete" ## vet: Run go vet for static analysis diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index 4be19b2a5..bf8f4104f 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -//go:generate cp -r ../../../../workspace . +//go:generate go run ../../../../scripts/copydir.go ../../../../workspace ./workspace //go:embed workspace var embeddedFiles embed.FS diff --git a/scripts/copydir.go b/scripts/copydir.go new file mode 100644 index 000000000..74eff6c72 --- /dev/null +++ b/scripts/copydir.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func main() { + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "usage: go run scripts/copydir.go \n") + os.Exit(2) + } + + src := os.Args[1] + dst := os.Args[2] + + if err := os.RemoveAll(dst); err != nil { + fmt.Fprintf(os.Stderr, "remove %s: %v\n", dst, err) + os.Exit(1) + } + + if err := copyTree(src, dst); err != nil { + fmt.Fprintf(os.Stderr, "copy %s -> %s: %v\n", src, dst, err) + os.Exit(1) + } +} + +func copyTree(src, dst string) error { + info, err := os.Stat(src) + if err != nil { + return err + } + if !info.IsDir() { + return fmt.Errorf("source is not a directory: %s", src) + } + + return filepath.Walk(src, func(path string, entry os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + target := dst + if rel != "." { + target = filepath.Join(dst, rel) + } + + if entry.IsDir() { + return os.MkdirAll(target, entry.Mode()) + } + + return copyFile(path, target, entry.Mode()) + }) +} + +func copyFile(src, dst string, mode os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Close() +} diff --git a/web/Makefile b/web/Makefile index 4dca810e7..254c439e9 100644 --- a/web/Makefile +++ b/web/Makefile @@ -2,15 +2,24 @@ build-android-arm64 build-android-bundle # Go variables -GO?=CGO_ENABLED=0 go +GO?=go WEB_GO?=$(GO) +CGO_ENABLED?=0 GO_BUILD_TAGS?=goolm,stdjson GOFLAGS?=-v -tags $(GO_BUILD_TAGS) +GOCACHE?=$(abspath ../.cache/go-build) +GOMODCACHE?=$(abspath ../.cache/go-mod) +GOTOOLCHAIN?=local +export CGO_ENABLED +export GOCACHE +export GOMODCACHE +export GOTOOLCHAIN # Build variables BUILD_DIR=build -OUTPUT?=$(BUILD_DIR)/picoclaw-launcher -OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64 +EXT= +OUTPUT?=$(BUILD_DIR)/picoclaw-launcher$(EXT) +OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64$(EXT) FRONTEND_DIR=frontend FRONTEND_INSTALL_STAMP=$(FRONTEND_DIR)/node_modules/.picoclaw-install-stamp BACKEND_DIR=backend @@ -19,18 +28,47 @@ PICOCLAW_BINARY_NAME=picoclaw PICOCLAW_BINARY?=$(abspath ../build/$(PICOCLAW_BINARY_NAME)) LAUNCHER_GUI_LDFLAG= +ifeq ($(OS),Windows_NT) + POWERSHELL=powershell -NoProfile -Command + WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) +endif + # Version -VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") -BUILD_TIME=$(shell date +%FT%T%z) -GO_VERSION=$(shell $(WEB_GO) version | awk '{print $$3}') +ifeq ($(OS),Windows_NT) + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>NUL)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>NUL)) + BUILD_TIME_RAW:=$(strip $(shell powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'")) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>NUL)) +else + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>/dev/null)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>/dev/null)) + BUILD_TIME_RAW:=$(strip $(shell date +%FT%T%z)) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>/dev/null)) +endif +VERSION?=$(if $(VERSION_RAW),$(VERSION_RAW),dev) +GIT_COMMIT=$(if $(GIT_COMMIT_RAW),$(GIT_COMMIT_RAW),dev) +BUILD_TIME=$(if $(BUILD_TIME_RAW),$(BUILD_TIME_RAW),dev) +GO_VERSION=$(if $(GO_VERSION_RAW),$(GO_VERSION_RAW),unknown) CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w # OS detection -UNAME_S:=$(shell uname -s) -UNAME_M:=$(shell uname -m) +ifeq ($(OS),Windows_NT) + UNAME_S=Windows + ifeq ($(WINDOWS_GOARCH_RAW),amd64) + UNAME_M=x86_64 + else ifeq ($(WINDOWS_GOARCH_RAW),arm64) + UNAME_M=arm64 + else ifeq ($(WINDOWS_GOARCH_RAW),386) + UNAME_M=x86 + else + UNAME_M=$(if $(WINDOWS_GOARCH_RAW),$(WINDOWS_GOARCH_RAW),x86_64) + endif +else + UNAME_S:=$(shell uname -s) + UNAME_M:=$(shell uname -m) +endif # Platform-specific settings ifeq ($(UNAME_S),Linux) @@ -62,7 +100,14 @@ else ifeq ($(UNAME_S),Darwin) endif else ifeq ($(UNAME_S),Windows) PLATFORM=windows - ARCH=$(UNAME_M) + ifeq ($(UNAME_M),x86_64) + ARCH=amd64 + else ifeq ($(UNAME_M),arm64) + ARCH=arm64 + else + ARCH=$(UNAME_M) + endif + EXT=.exe PICOCLAW_BINARY_NAME=picoclaw.exe LAUNCHER_GUI_LDFLAG=-H=windowsgui else @@ -91,21 +136,36 @@ dev-backend: # Build frontend and embed into Go binary build: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path (Split-Path -Parent '$(OUTPUT)') | Out-Null" +else @mkdir -p "$$(dirname "$(OUTPUT)")" +endif ${WEB_GO} build $(GOFLAGS) -ldflags "$(LAUNCHER_LDFLAGS)" -o "$(OUTPUT)" ./$(BACKEND_DIR)/ # Build launcher for Android ARM64 (frontend must already be built) build-android-arm64: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +else @mkdir -p $(BUILD_DIR) +endif GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o "$(OUTPUT_ANDROID_ARM64)" ./$(BACKEND_DIR)/ # Build launcher for all Android architectures build-android-bundle: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +else @mkdir -p $(BUILD_DIR) +endif GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o "$(BUILD_DIR)/picoclaw-launcher-android-arm64" ./$(BACKEND_DIR)/ @echo "All Android launcher builds complete" build-frontend: +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" +else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ @@ -115,12 +175,17 @@ build-frontend: (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ fi +endif @echo "Building frontend..." @cd $(FRONTEND_DIR) && pnpm build:backend build-dev-picoclaw: @echo "Building picoclaw for launcher development..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path (Split-Path -Parent '$(PICOCLAW_BINARY)') | Out-Null" +else @mkdir -p "$$(dirname "$(PICOCLAW_BINARY)")" +endif @$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o "$(PICOCLAW_BINARY)" ../cmd/picoclaw # Run all tests @@ -135,5 +200,10 @@ lint: # Clean build artifacts clean: +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "$$paths=@('$(FRONTEND_DIR)/dist','$(BACKEND_DIST)','$(BUILD_DIR)'); foreach($$p in $$paths){ if (Test-Path -LiteralPath $$p) { Remove-Item -LiteralPath $$p -Recurse -Force } }" + @node $(FRONTEND_DIR)/scripts/ensure-backend-gitkeep.cjs +else rm -rf $(FRONTEND_DIR)/dist $(BACKEND_DIST) $(BUILD_DIR) node $(FRONTEND_DIR)/scripts/ensure-backend-gitkeep.cjs +endif