Skip to content

Commit 75d5aab

Browse files
committed
fix: correctly cache ush files
1 parent 254c042 commit 75d5aab

File tree

5 files changed

+275
-67
lines changed

5 files changed

+275
-67
lines changed

cmd/build.go

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ func runBuild(cmd *cobra.Command, args []string) error {
6161
if err != nil {
6262
fmt.Fprintf(os.Stderr, "Warning: Cache lookup failed: %v\n", err)
6363
} else if entry != nil && entry.Success {
64-
// Cache hit!
65-
outputDir := getOutputDir(absFile)
66-
if err := buildCache.Restore(entry, outputDir); err != nil {
64+
// Cache hit! Restore to source directory
65+
sourceDir := filepath.Dir(absFile)
66+
if err := buildCache.Restore(entry, sourceDir); err != nil {
6767
fmt.Fprintf(os.Stderr, "Warning: Failed to restore from cache: %v\n", err)
6868
} else {
6969
if cfg.Verbose {
@@ -84,16 +84,14 @@ func runBuild(cmd *cobra.Command, args []string) error {
8484
success = false
8585
// Store failed build in cache too (so we don't retry immediately)
8686
if !noCache && buildCache != nil {
87-
outputDir := getOutputDir(absFile)
88-
_ = buildCache.Store(absFile, cfg, outputDir, false)
87+
_ = buildCache.Store(absFile, cfg, false)
8988
}
9089
return err
9190
}
9291

9392
// Store successful build in cache
9493
if !noCache && buildCache != nil {
95-
outputDir := getOutputDir(absFile)
96-
if err := buildCache.Store(absFile, cfg, outputDir, success); err != nil {
94+
if err := buildCache.Store(absFile, cfg, success); err != nil {
9795
fmt.Fprintf(os.Stderr, "Warning: Failed to cache build: %v\n", err)
9896
}
9997
}
@@ -119,9 +117,3 @@ func compileSingle(cfg *config.Config, sourceFile string) error {
119117
// Execute the compiler command
120118
return builder.ExecuteCommand(cfg.CompilerPath, cmdArgs)
121119
}
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-
}

internal/cache/artifacts.go

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
// Package cache provides artifact management for SIMPL+ build caching.
2+
//
3+
// The Crestron SIMPL+ compiler (SPlusCC.exe) generates output files in two locations:
4+
// 1. A shared "SPlsWork" directory adjacent to source files (for most artifacts)
5+
// 2. A .ush header file placed adjacent to the source file itself
6+
//
7+
// Multiple SIMPL+ source files in the same directory all output to the same SPlsWork
8+
// folder, making it critical to filter artifacts by source file name when caching.
9+
//
10+
// For a source file named "example.usp", the compiler generates:
11+
// - Adjacent: example.ush (header file in same directory as source)
12+
// - SPlsWork: example.dll, example.cs, example.inf
13+
// - SPlsWork: S2_example.c, S2_example.h, S2_example.elf, etc.
14+
//
15+
// This package handles selective copying/restoration of only the artifacts
16+
// belonging to a specific source file, ignoring shared libraries and other
17+
// source files' artifacts.
118
package cache
219

320
import (
@@ -7,14 +24,15 @@ import (
724
"path/filepath"
825
)
926

10-
// CopyArtifacts copies compiled outputs from source to cache
11-
func CopyArtifacts(sourceDir, destDir string, outputs []string) error {
27+
// CopyArtifacts copies compiled outputs from a base directory to cache
28+
// The outputs paths are relative to baseDir (e.g., "SPlsWork/example.dll", "example.ush")
29+
func CopyArtifacts(baseDir, destDir string, outputs []string) error {
1230
if err := os.MkdirAll(destDir, 0o755); err != nil {
1331
return fmt.Errorf("failed to create artifact directory: %w", err)
1432
}
1533

1634
for _, output := range outputs {
17-
src := filepath.Join(sourceDir, output)
35+
src := filepath.Join(baseDir, output)
1836
dst := filepath.Join(destDir, output)
1937

2038
if err := copyFile(src, dst); err != nil {
@@ -25,16 +43,18 @@ func CopyArtifacts(sourceDir, destDir string, outputs []string) error {
2543
return nil
2644
}
2745

28-
// RestoreArtifacts copies cached outputs back to the working directory
46+
// RestoreArtifacts copies cached outputs back to the base directory
47+
// The outputs paths are relative to destDir (e.g., "SPlsWork/example.dll", "example.ush")
2948
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-
3449
for _, output := range outputs {
3550
src := filepath.Join(cacheDir, output)
3651
dst := filepath.Join(destDir, output)
3752

53+
// Create parent directory if needed (e.g., for SPlsWork/...)
54+
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
55+
return fmt.Errorf("failed to create output directory: %w", err)
56+
}
57+
3858
if err := copyFile(src, dst); err != nil {
3959
return fmt.Errorf("failed to restore %s: %w", output, err)
4060
}
@@ -43,33 +63,81 @@ func RestoreArtifacts(cacheDir, destDir string, outputs []string) error {
4363
return nil
4464
}
4565

46-
// CollectOutputs scans a directory and returns a list of compiled output files
47-
func CollectOutputs(dir string) ([]string, error) {
66+
// CollectOutputs scans for compiled output files specific to the given source file.
67+
// It checks two locations:
68+
// 1. The source file directory for .ush header files
69+
// 2. The SPlsWork directory for other artifacts
70+
//
71+
// Returns paths relative to the source directory (e.g., "example.ush", "SPlsWork/example.dll")
72+
func CollectOutputs(sourceFile string) ([]string, error) {
4873
var outputs []string
4974

50-
entries, err := os.ReadDir(dir)
75+
// Extract base name without extension (e.g., "example1" from "example1.usp")
76+
baseName := filepath.Base(sourceFile)
77+
baseName = baseName[:len(baseName)-len(filepath.Ext(baseName))]
78+
79+
sourceDir := filepath.Dir(sourceFile)
80+
splsWorkDir := filepath.Join(sourceDir, "SPlsWork")
81+
82+
// Check for .ush file adjacent to source
83+
ushFile := baseName + ".ush"
84+
ushPath := filepath.Join(sourceDir, ushFile)
85+
if _, err := os.Stat(ushPath); err == nil {
86+
outputs = append(outputs, ushFile)
87+
}
88+
89+
// Scan SPlsWork directory
90+
entries, err := os.ReadDir(splsWorkDir)
5191
if err != nil {
5292
if os.IsNotExist(err) {
53-
return nil, nil // No outputs yet
93+
return outputs, nil // No SPlsWork directory yet
5494
}
55-
return nil, fmt.Errorf("failed to read output directory: %w", err)
95+
return nil, fmt.Errorf("failed to read SPlsWork directory: %w", err)
5696
}
5797

5898
for _, entry := range entries {
5999
if entry.IsDir() {
60100
continue
61101
}
62102

63-
// Only collect actual output files, skip metadata
64103
name := entry.Name()
65-
if name != "metadata.json" {
66-
outputs = append(outputs, name)
104+
105+
// Skip metadata files
106+
if name == "metadata.json" {
107+
continue
108+
}
109+
110+
// Check if this file belongs to our source file
111+
// Match patterns: {basename}.* or S2_{basename}.*
112+
if isOutputFile(name, baseName) {
113+
// Store with SPlsWork/ prefix for proper path handling
114+
outputs = append(outputs, filepath.Join("SPlsWork", name))
67115
}
68116
}
69117

70118
return outputs, nil
71119
}
72120

121+
// isOutputFile checks if a filename belongs to the given source base name
122+
func isOutputFile(filename, baseName string) bool {
123+
fileBase := filename[:len(filename)-len(filepath.Ext(filename))]
124+
125+
// Direct match: example1.dll, example1.cs, etc.
126+
if fileBase == baseName {
127+
return true
128+
}
129+
130+
// Target-prefixed match: S2_example1.c, S2_example1.h, etc.
131+
if len(fileBase) > 3 && fileBase[0] == 'S' && fileBase[2] == '_' {
132+
// Extract after "S2_" prefix
133+
if fileBase[3:] == baseName {
134+
return true
135+
}
136+
}
137+
138+
return false
139+
}
140+
73141
// copyFile copies a file from src to dst
74142
func copyFile(src, dst string) error {
75143
srcFile, err := os.Open(src)

internal/cache/cache.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Package cache provides build caching functionality for SIMPL+ compilation.
2+
//
3+
// The cache system addresses the challenge of shared output directories (SPlsWork)
4+
// where the Crestron compiler places artifacts from multiple source files together.
5+
// Rather than caching entire directories, the cache:
6+
//
7+
// 1. Filters artifacts by source file name (e.g., example1.dll, S2_example1.c)
8+
// 2. Stores only relevant artifacts per source file in separate cache entries
9+
// 3. Uses SHA256 hashing of source content + configuration for cache keys
10+
// 4. Stores metadata in BoltDB and artifacts in the filesystem
11+
//
12+
// This allows incremental compilation where each source file can be cached
13+
// and restored independently, even when multiple files share the same output directory.
114
package cache
215

316
import (
@@ -106,14 +119,14 @@ func (c *Cache) Get(sourceFile string, cfg *config.Config) (*Entry, error) {
106119
}
107120

108121
// Store saves a cache entry and copies artifacts
109-
func (c *Cache) Store(sourceFile string, cfg *config.Config, outputDir string, success bool) error {
122+
func (c *Cache) Store(sourceFile string, cfg *config.Config, success bool) error {
110123
hash, err := HashSource(sourceFile, cfg)
111124
if err != nil {
112125
return fmt.Errorf("failed to hash source: %w", err)
113126
}
114127

115-
// Collect outputs from build directory
116-
outputs, err := CollectOutputs(outputDir)
128+
// Collect outputs from both source dir and SPlsWork dir
129+
outputs, err := CollectOutputs(sourceFile)
117130
if err != nil {
118131
return fmt.Errorf("failed to collect outputs: %w", err)
119132
}
@@ -145,18 +158,19 @@ func (c *Cache) Store(sourceFile string, cfg *config.Config, outputDir string, s
145158
return fmt.Errorf("failed to store cache entry: %w", err)
146159
}
147160

148-
// Copy artifacts to cache
161+
// Copy artifacts to cache (outputs are relative to source directory)
149162
if success && len(outputs) > 0 {
150163
artifactDir := c.artifactDir(hash)
151-
if err := CopyArtifacts(outputDir, artifactDir, outputs); err != nil {
164+
sourceDir := filepath.Dir(sourceFile)
165+
if err := CopyArtifacts(sourceDir, artifactDir, outputs); err != nil {
152166
return fmt.Errorf("failed to copy artifacts: %w", err)
153167
}
154168
}
155169

156170
return nil
157171
}
158172

159-
// Restore copies cached artifacts back to the output directory
173+
// Restore copies cached artifacts back to the source directory
160174
func (c *Cache) Restore(entry *Entry, destDir string) error {
161175
if !entry.Success || len(entry.Outputs) == 0 {
162176
return fmt.Errorf("cannot restore failed build or build with no outputs")

0 commit comments

Comments
 (0)