Commit 18ed1dc
docs/CLAUDE.md
@@ -37,18 +37,13 @@ This is a Go web application for "buylater.email" - an email-based delayed grati
## Development Commands
-**Makefile Available**: Use `make help` to see all available targets.
-
-### Quick Commands (via Makefile)
+### Makefile Commands (Recommended)
```bash
-make run # Start development server
-make build # Build application binary
-make test # Run tests
+make build # Build application binary (with vendor unpack)
+make test # Run tests (with vendor unpack)
make fmt # Format code
-make vet # Vet code
-make check # Run fmt, vet, and test
-make clean # Remove binaries
-make tidy # Clean up dependencies
+make lint # Lint code (vet + format check)
+make update # Update dependencies and vendor pack
```
### Direct Go Commands
tools/vendor/pack.go
@@ -0,0 +1,96 @@
+//go:build ignore
+
+// Command tools/vendor/pack.go creates `vendor.tar.gz` from the current
+// project `vendor` directory. Intended for use as a build support tool.
+package main
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/fs"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+// vendor.tar.gz pack
+func main() {
+ const (
+ archiveName = "vendor.tar.gz"
+ vendorDir = "vendor"
+ vendorDirMode = 0o755
+ )
+
+ // discover this file's location
+ _, callerFilepath, _, callerFound := runtime.Caller(0)
+ if !callerFound {
+ slog.Error("failed to find caller location")
+ return
+ }
+ archivePath := filepath.Join(filepath.Dir(callerFilepath), archiveName)
+
+ archiveFile, err := os.Create(archivePath)
+ if err != nil {
+ slog.Error("failed to create output tarball",
+ slog.String("err", err.Error()),
+ slog.String("path", archivePath))
+ return
+ }
+ defer func() { _ = archiveFile.Close() }()
+
+ gzw := gzip.NewWriter(archiveFile)
+ defer func() { _ = gzw.Close() }()
+
+ tw := tar.NewWriter(gzw)
+ defer func() { _ = tw.Close() }()
+
+ err = filepath.WalkDir(
+ vendorDir,
+ func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return fmt.Errorf("failed to walk vendor dir path=%q: %w", path, err)
+ }
+ relPath, err := filepath.Rel(vendorDir, path)
+ if err != nil {
+ return fmt.Errorf("failed to create relative path path=%q: %w", path, err)
+ }
+ if relPath == "." {
+ return nil
+ }
+ info, err := d.Info()
+ if err != nil {
+ return fmt.Errorf("failed to get file info path=%q: %w", path, err)
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ return nil
+ }
+ header, err := tar.FileInfoHeader(info, "")
+ if err != nil {
+ return fmt.Errorf("failed to create tar header path=%q: %w", path, err)
+ }
+ header.Name = relPath
+ err = tw.WriteHeader(header)
+ if err != nil {
+ return fmt.Errorf("failed to write tar header path=%q: %w", path, err)
+ }
+ if info.IsDir() {
+ return nil
+ }
+ vendorFile, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed to open vendor file path=%q: %w", path, err)
+ }
+ _, err = io.Copy(tw, vendorFile)
+ if err != nil {
+ return fmt.Errorf("failed to write vendor file to tar path=%q: %w", path, err)
+ }
+ return nil
+ })
+ if err != nil {
+ slog.Error("failed to package vendor directory",
+ slog.String("err", err.Error()))
+ }
+}
tools/vendor/README.md
@@ -0,0 +1,24 @@
+# vendor packing tools
+
+This directory contains two helper scripts to manage a compressed `vendor.tar.gz` archive of the project's `vendor` directory. This keeps the project commits clean from vendor changes, while preserving repeatable builds.
+
+## Files
+
+- `pack.go` – Creates `vendor.tar.gz` from the current `vendor` directory.
+- `unpack.go` – Extracts `vendor.tar.gz` into the `vendor` directory.
+
+## Usage
+
+Run from the project root:
+
+```bash
+go run tools/vendor/pack.go # Archive the vendor directory
+go run tools/vendor/unpack.go # Extract the vendor archive
+```
+
+## Design Decisions
+
+- Designed to avoid committing all of the `vendor` changes directly within the repo
+- Enables reproducible builds with vendored, `git-lfs` stored `vendor.tar.gz`
+- No external tools: Pure Go. No make, make-alternatives, no shell dependencies.
+- Portable & clean: `go build` still works if you don't use this tool
tools/vendor/unpack.go
@@ -0,0 +1,106 @@
+//go:build ignore
+
+// Command tools/vendor/unpack.go extracts `vendor.tar.gz` into the current
+// project `vendor` directory. Intended for use as a build support tool.
+package main
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "errors"
+ "io"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+// vendor.tar.gz unpack
+func main() {
+ const (
+ archiveName = "vendor.tar.gz"
+ vendorDir = "vendor"
+ vendorDirMode = 0o755
+ )
+
+ // discover this file's location
+ _, callerFilepath, _, callerFound := runtime.Caller(0)
+ if !callerFound {
+ slog.Error("failed to find caller location")
+ return
+ }
+ archivePath := filepath.Join(filepath.Dir(callerFilepath), archiveName)
+
+ archiveFile, err := os.Open(archivePath)
+ if err != nil {
+ slog.Error("failed to open vendor tarball",
+ slog.String("err", err.Error()),
+ slog.String("path", archivePath))
+ return
+ }
+ defer func() { _ = archiveFile.Close() }()
+
+ gzr, err := gzip.NewReader(archiveFile)
+ if err != nil {
+ slog.Error("failed to open gzip reader",
+ slog.String("err", err.Error()),
+ slog.String("path", archivePath))
+ return
+ }
+ defer func() { _ = gzr.Close() }()
+
+ tr := tar.NewReader(gzr)
+ for {
+ header, err := tr.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ slog.Error("failed to read next tar entry",
+ slog.String("err", err.Error()))
+ }
+ info := header.FileInfo()
+
+ if !info.IsDir() {
+ target := filepath.Join(vendorDir, header.Name)
+ dir := filepath.Dir(target)
+
+ err = os.MkdirAll(dir, vendorDirMode)
+ if err != nil {
+ slog.Error("failed to create vendor dir",
+ slog.String("err", err.Error()),
+ slog.String("dir", dir))
+ return
+ }
+
+ vendorFile, err := os.OpenFile(
+ target,
+ os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
+ info.Mode())
+ if err != nil {
+ slog.Error("failed to open vendor file",
+ slog.String("err", err.Error()),
+ slog.String("path", target))
+ _ = vendorFile.Close()
+ return
+ }
+
+ _, err = io.Copy(vendorFile, tr)
+ if err != nil {
+ slog.Error("failed to copy vendor file from tar",
+ slog.String("err", err.Error()),
+ slog.String("path", target))
+ _ = vendorFile.Close()
+ return
+ }
+
+ err = vendorFile.Close()
+ if err != nil {
+ slog.Error("failed to close vendor file",
+ slog.String("err", err.Error()),
+ slog.String("path", target))
+ return
+ }
+ }
+ }
+}
tools/vendor/vendor.tar.gz
Binary file
Makefile
@@ -1,72 +1,43 @@
# Makefile for buylater.email project
-.PHONY: build run test fmt vet clean help
+.PHONY: build lint fmt update test
# Variables
BINARY_NAME=buylater
BINARY_PATH=./cmd/web
BUILD_DIR=./bin
-# Default target
-all: build
-
# Build the application
build:
+ @echo "Unpacking vendor dependencies..."
+ @go run tools/vendor/unpack.go
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
@go build -o $(BUILD_DIR)/$(BINARY_NAME) $(BINARY_PATH)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
-# Run the application for development
-run:
- @echo "Starting development server..."
- @go run $(BINARY_PATH)/*.go
-
-# Run tests
-test:
- @echo "Running tests..."
- @go test ./...
+# Lint code (vet + format check)
+lint:
+ @echo "Linting code..."
+ @go vet ./...
+ @test -z "$$(gofmt -l .)" || (echo "Code not formatted, run 'make fmt'" && exit 1)
# Format code
fmt:
@echo "Formatting code..."
@go fmt ./...
-# Vet code for potential issues
-vet:
- @echo "Vetting code..."
- @go vet ./...
-
-# Clean up binaries
-clean:
- @echo "Cleaning up..."
- @rm -rf $(BUILD_DIR)
- @rm -f $(BINARY_NAME)
- @echo "Clean complete"
-
-# Tidy up dependencies
-tidy:
- @echo "Tidying dependencies..."
+# Update dependencies and vendor
+update:
+ @echo "Updating dependencies..."
@go mod tidy
+ @go mod vendor
+ @echo "Packing vendor dependencies..."
+ @go run tools/vendor/pack.go
+ @echo "Dependencies updated and vendored"
-# Run all quality checks
-check: fmt vet test
- @echo "All checks passed!"
-
-# Install development dependencies
-deps:
- @echo "Downloading dependencies..."
- @go mod download
-
-# Show help
-help:
- @echo "Available targets:"
- @echo " build - Build the application binary"
- @echo " run - Run the application for development"
- @echo " test - Run all tests"
- @echo " fmt - Format all Go code"
- @echo " vet - Run go vet on all code"
- @echo " clean - Remove built binaries"
- @echo " tidy - Clean up go.mod dependencies"
- @echo " check - Run fmt, vet, and test"
- @echo " deps - Download dependencies"
- @echo " help - Show this help message"
\ No newline at end of file
+# Run tests
+test:
+ @echo "Unpacking vendor dependencies..."
+ @go run tools/vendor/unpack.go
+ @echo "Running tests..."
+ @go test ./...
\ No newline at end of file