Skip to content

Commit b1791be

Browse files
committed
feat: implement caching
1 parent 9b4d7e9 commit b1791be

File tree

10 files changed

+762
-3
lines changed

10 files changed

+762
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ bin/
3737
build/
3838
dist/
3939

40+
# Cache
41+
.spc-cache/
42+
4043
# Local History
4144
.history/
4245

cmd/build.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57

8+
"github.com/Norgate-AV/spc/internal/cache"
69
"github.com/Norgate-AV/spc/internal/compiler"
710
"github.com/Norgate-AV/spc/internal/config"
811
"github.com/Norgate-AV/spc/internal/utils"
@@ -29,19 +32,96 @@ func runBuild(cmd *cobra.Command, args []string) error {
2932
return err
3033
}
3134

32-
// Build compiler command arguments
35+
// Check if cache is disabled
36+
noCache, _ := cmd.Flags().GetBool("no-cache")
37+
38+
// Initialize cache (unless disabled)
39+
var buildCache *cache.Cache
40+
if !noCache {
41+
buildCache, err = cache.New("")
42+
if err != nil {
43+
fmt.Fprintf(os.Stderr, "Warning: Failed to initialize cache: %v\n", err)
44+
// Continue without cache
45+
noCache = true
46+
} else {
47+
defer buildCache.Close()
48+
}
49+
}
50+
51+
// Process each source file
52+
for _, file := range args {
53+
absFile, err := filepath.Abs(file)
54+
if err != nil {
55+
return fmt.Errorf("failed to resolve path for %s: %w", file, err)
56+
}
57+
58+
// Check cache (if enabled)
59+
if !noCache && buildCache != nil {
60+
entry, err := buildCache.Get(absFile, cfg)
61+
if err != nil {
62+
fmt.Fprintf(os.Stderr, "Warning: Cache lookup failed: %v\n", err)
63+
} else if entry != nil && entry.Success {
64+
// Cache hit!
65+
outputDir := getOutputDir(absFile)
66+
if err := buildCache.Restore(entry, outputDir); err != nil {
67+
fmt.Fprintf(os.Stderr, "Warning: Failed to restore from cache: %v\n", err)
68+
} else {
69+
if cfg.Verbose {
70+
fmt.Printf("✓ Using cached build for %s\n", filepath.Base(file))
71+
}
72+
continue // Skip compilation
73+
}
74+
}
75+
}
76+
77+
// Cache miss or disabled - compile
78+
if cfg.Verbose {
79+
fmt.Printf("Compiling %s...\n", filepath.Base(file))
80+
}
81+
82+
success := true
83+
if err := compileSingle(cfg, absFile); err != nil {
84+
success = false
85+
// Store failed build in cache too (so we don't retry immediately)
86+
if !noCache && buildCache != nil {
87+
outputDir := getOutputDir(absFile)
88+
_ = buildCache.Store(absFile, cfg, outputDir, false)
89+
}
90+
return err
91+
}
92+
93+
// Store successful build in cache
94+
if !noCache && buildCache != nil {
95+
outputDir := getOutputDir(absFile)
96+
if err := buildCache.Store(absFile, cfg, outputDir, success); err != nil {
97+
fmt.Fprintf(os.Stderr, "Warning: Failed to cache build: %v\n", err)
98+
}
99+
}
100+
}
101+
102+
return nil
103+
}
104+
105+
// compileSingle compiles a single source file
106+
func compileSingle(cfg *config.Config, sourceFile string) error {
33107
builder := compiler.NewCommandBuilder()
34-
cmdArgs, err := builder.BuildCommandArgs(cfg, args)
108+
cmdArgs, err := builder.BuildCommandArgs(cfg, []string{sourceFile})
35109
if err != nil {
36110
return err
37111
}
38112

39113
// Print build info if verbose mode is enabled
40114
if cfg.Verbose {
41115
series := utils.ParseTarget(cfg.Target)
42-
builder.PrintBuildInfo(cfg, series, args, cmdArgs)
116+
builder.PrintBuildInfo(cfg, series, []string{sourceFile}, cmdArgs)
43117
}
44118

45119
// Execute the compiler command
46120
return builder.ExecuteCommand(cfg.CompilerPath, cmdArgs)
47121
}
122+
123+
// getOutputDir returns the SPlsWork directory for a source file
124+
func getOutputDir(sourceFile string) string {
125+
dir := filepath.Dir(sourceFile)
126+
return filepath.Join(dir, "SPlsWork")
127+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func init() {
3232
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output")
3333
rootCmd.PersistentFlags().StringP("out", "o", "", "Output file for compilation logs")
3434
rootCmd.PersistentFlags().StringSliceP("usersplusfolder", "u", []string{}, "User SIMPL+ folders")
35+
rootCmd.PersistentFlags().Bool("no-cache", false, "Disable build cache")
3536
rootCmd.AddCommand(buildCmd)
3637

3738
viper.SetDefault("compiler_path", "C:/Program Files (x86)/Crestron/Simpl/SPlusCC.exe")

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ require (
478478
gitlab.com/gitlab-org/api/client-go v0.148.1 // indirect
479479
go-simpler.org/musttag v0.13.0 // indirect
480480
go-simpler.org/sloglint v0.9.0 // indirect
481+
go.etcd.io/bbolt v1.4.3 // indirect
481482
go.mongodb.org/mongo-driver v1.17.3 // indirect
482483
go.opencensus.io v0.24.0 // indirect
483484
go.opentelemetry.io/auto/sdk v1.1.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,8 @@ go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE=
14901490
go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM=
14911491
go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE=
14921492
go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww=
1493+
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
1494+
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
14931495
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
14941496
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
14951497
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

internal/cache/artifacts.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// CopyArtifacts copies compiled outputs from source to cache
11+
func CopyArtifacts(sourceDir, destDir string, outputs []string) error {
12+
if err := os.MkdirAll(destDir, 0o755); err != nil {
13+
return fmt.Errorf("failed to create artifact directory: %w", err)
14+
}
15+
16+
for _, output := range outputs {
17+
src := filepath.Join(sourceDir, output)
18+
dst := filepath.Join(destDir, output)
19+
20+
if err := copyFile(src, dst); err != nil {
21+
return fmt.Errorf("failed to copy %s: %w", output, err)
22+
}
23+
}
24+
25+
return nil
26+
}
27+
28+
// RestoreArtifacts copies cached outputs back to the working directory
29+
func RestoreArtifacts(cacheDir, destDir string, outputs []string) error {
30+
if err := os.MkdirAll(destDir, 0o755); err != nil {
31+
return fmt.Errorf("failed to create output directory: %w", err)
32+
}
33+
34+
for _, output := range outputs {
35+
src := filepath.Join(cacheDir, output)
36+
dst := filepath.Join(destDir, output)
37+
38+
if err := copyFile(src, dst); err != nil {
39+
return fmt.Errorf("failed to restore %s: %w", output, err)
40+
}
41+
}
42+
43+
return nil
44+
}
45+
46+
// CollectOutputs scans a directory and returns a list of compiled output files
47+
func CollectOutputs(dir string) ([]string, error) {
48+
var outputs []string
49+
50+
entries, err := os.ReadDir(dir)
51+
if err != nil {
52+
if os.IsNotExist(err) {
53+
return nil, nil // No outputs yet
54+
}
55+
return nil, fmt.Errorf("failed to read output directory: %w", err)
56+
}
57+
58+
for _, entry := range entries {
59+
if entry.IsDir() {
60+
continue
61+
}
62+
63+
// Only collect actual output files, skip metadata
64+
name := entry.Name()
65+
if name != "metadata.json" {
66+
outputs = append(outputs, name)
67+
}
68+
}
69+
70+
return outputs, nil
71+
}
72+
73+
// copyFile copies a file from src to dst
74+
func copyFile(src, dst string) error {
75+
srcFile, err := os.Open(src)
76+
if err != nil {
77+
return err
78+
}
79+
defer srcFile.Close()
80+
81+
// Create parent directory if needed
82+
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
83+
return err
84+
}
85+
86+
dstFile, err := os.Create(dst)
87+
if err != nil {
88+
return err
89+
}
90+
defer dstFile.Close()
91+
92+
if _, err := io.Copy(dstFile, srcFile); err != nil {
93+
return err
94+
}
95+
96+
// Preserve file permissions
97+
srcInfo, err := os.Stat(src)
98+
if err != nil {
99+
return err
100+
}
101+
102+
return os.Chmod(dst, srcInfo.Mode())
103+
}

0 commit comments

Comments
 (0)