@@ -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
3435var (
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
4147func 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
258320func NormalizeArchitecture (arch string ) string {
259321 archMap := map [string ]string {
0 commit comments