Skip to content

Commit 1e52dd0

Browse files
committed
feat(cmd): detect changed in configuration
Signed-off-by: AtomicFS <vojtech.vesely@9elements.com>
1 parent 01aa72a commit 1e52dd0

File tree

2 files changed

+82
-7
lines changed

2 files changed

+82
-7
lines changed

cmd/firmware-action/recipes/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"dagger.io/dagger"
1818
"github.com/9elements/firmware-action/cmd/firmware-action/container"
19+
"github.com/9elements/firmware-action/cmd/firmware-action/filesystem"
1920
"github.com/9elements/firmware-action/cmd/firmware-action/logging"
2021
"github.com/go-playground/validator/v10"
2122
)
@@ -313,6 +314,18 @@ type FirmwareModule interface {
313314
buildFirmware(ctx context.Context, client *dagger.Client) error
314315
}
315316

317+
// ===============================
318+
// Functions for FirmwareModules
319+
// ===============================
320+
321+
// FilenameForFirmwareModule is used to take a user-defined module name and make it into filename, removing
322+
// all problematic characters
323+
func FilenameForFirmwareModule(name string) string {
324+
// For example:
325+
// "Coreboot Example" should return "Coreboot_Example.json"
326+
return fmt.Sprintf("%s.json", filesystem.Filenamify(name))
327+
}
328+
316329
// ======================
317330
// Functions for Config
318331
// ======================

cmd/firmware-action/recipes/recipes.go

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"dagger.io/dagger"
1818
"github.com/9elements/firmware-action/cmd/firmware-action/filesystem"
19+
"github.com/google/go-cmp/cmp"
1920
"github.com/heimdalr/dag"
2021
)
2122

@@ -34,8 +35,13 @@ var (
3435
var (
3536
// ContainerWorkDir specifies directory in container used as work directory
3637
ContainerWorkDir = "/workdir"
38+
// StatusDir is directory for temporary files generated by firmware-action to aid change detection
39+
StatusDir = ".firmware-action"
3740
// TimestampsDir specifies directory for timestamps to detect changes in sources
38-
TimestampsDir = ".firmware-action/timestamps"
41+
TimestampsDir = filepath.Join(StatusDir, "timestamps")
42+
// CompiledConfigsDir specifies directory for successfully compiled module configurations to detect changes in
43+
// configuration
44+
CompiledConfigsDir = filepath.Join(StatusDir, "configs")
3945
)
4046

4147
func forestAddVertex(forest *dag.DAG, key string, value FirmwareModule, dependencies [][]string) ([][]string, error) {
@@ -179,13 +185,17 @@ func Execute(ctx context.Context, target string, config *Config) error {
179185
if err != nil {
180186
return err
181187
}
188+
err = os.MkdirAll(CompiledConfigsDir, os.ModePerm)
189+
if err != nil {
190+
return err
191+
}
182192

183193
// Find requested target
184194
modules := config.AllModules()
185195
if _, ok := modules[target]; ok {
186-
// Check if up-to-date
187-
// Either returns time, or zero time and error
188-
// zero time means there was no previous run
196+
// Check for any change in source files
197+
// Either returns time, or zero time and error
198+
// zero time means there was no previous run
189199
timestampFile := filepath.Join(TimestampsDir, fmt.Sprintf("%s.txt", target))
190200
lastRun, _ := filesystem.LoadLastRunTime(timestampFile)
191201

@@ -199,19 +209,47 @@ func Execute(ctx context.Context, target string, config *Config) error {
199209
}
200210
}
201211

212+
// Check for any change in configuration
213+
// I did consider to save only the small struct related to each module, but it was
214+
// proving to be far too much work. Instead we save the whole configuration file (for each module
215+
// separately) and only compare the relevant modules between these two configurations
216+
oldConfigPath := filepath.Join(CompiledConfigsDir, FilenameForFirmwareModule(target))
217+
err = filesystem.CheckFileExists(oldConfigPath)
218+
changedConfig := false
219+
if errors.Is(err, os.ErrExist) {
220+
oldConfig, err := ReadConfig(oldConfigPath)
221+
if err != nil {
222+
return err
223+
}
224+
oldModules := oldConfig.AllModules()
225+
changedConfig = !cmp.Equal(modules[target], oldModules[target])
226+
}
227+
slog.Warn("Changes detected",
228+
slog.Bool("sources", changesDetected),
229+
slog.Bool("config", changedConfig),
230+
)
231+
202232
// Check if output directory already exist
203233
// We want to skip build if the output directory exists and is not empty
204234
// If it is empty, then just continue with the building
205235
// If changes in sources were detected, re-build
206236
_, errExists := os.Stat(modules[target].GetOutputDir())
207237
empty, _ := IsDirEmpty(modules[target].GetOutputDir())
208238
if errExists == nil && !empty {
209-
if changesDetected {
239+
if changesDetected || changedConfig {
210240
// If any of the sources changed, we need to rebuild
211241
os.RemoveAll(modules[target].GetOutputDir())
212242
} else {
213243
// Is already up-to-date
214244
slog.Warn(fmt.Sprintf("Target '%s' is up-to-date, skipping build", target))
245+
246+
// It is possible that the timestamp of old config files are missing
247+
// for example user deleted them, CI does not cache them, ...
248+
// but at the same time the output directory exists with all the artifacts
249+
// The 'override' is false, meaning that if files already exist, they will not
250+
// be overridden
251+
saveCheckpointTimeStamp(timestampFile, false)
252+
saveCheckpointConfig(oldConfigPath, config, false)
215253
return ErrBuildUpToDate
216254
}
217255
}
@@ -246,14 +284,38 @@ func Execute(ctx context.Context, target string, config *Config) error {
246284
// Build the module
247285
err = modules[target].buildFirmware(ctx, client)
248286
if err == nil {
249-
// On success update the timestamp
250-
_ = filesystem.SaveCurrentRunTime(timestampFile)
287+
// On successful build, save timestamp and current configuration
288+
saveCheckpointTimeStamp(timestampFile, true)
289+
saveCheckpointConfig(oldConfigPath, config, true)
251290
}
252291
return err
253292
}
254293
return ErrTargetMissing
255294
}
256295

296+
func saveCheckpointTimeStamp(timestampFilePath string, override bool) {
297+
// On success update the timestamp
298+
err := filesystem.CheckFileExists(timestampFilePath)
299+
if errors.Is(err, os.ErrNotExist) || override {
300+
slog.Debug("Saving timestamp")
301+
_ = filesystem.SaveCurrentRunTime(timestampFilePath)
302+
}
303+
}
304+
305+
func saveCheckpointConfig(configPath string, config *Config, override bool) {
306+
// On success update the old configuration
307+
err := filesystem.CheckFileExists(configPath)
308+
if errors.Is(err, os.ErrNotExist) || override {
309+
slog.Debug("Saving copy of configuration file")
310+
err = WriteConfig(configPath, config)
311+
if err != nil {
312+
slog.Warn("Failed to create a snapshot of configuration for detecting future changes",
313+
slog.Any("error", err),
314+
)
315+
}
316+
}
317+
}
318+
257319
// NormalizeArchitecture will translate various architecture strings into expected format
258320
func NormalizeArchitecture(arch string) string {
259321
archMap := map[string]string{

0 commit comments

Comments
 (0)