From edd0b86ba4daa1a6109ba9a90aebdce9e2178c2f Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 12 Aug 2025 15:35:20 +0200 Subject: [PATCH 01/13] makeSourceFile is now a method of SketchLibrariesDetector --- .../builder/internal/detector/detector.go | 31 +++++++++++++++++-- .../builder/internal/detector/source_file.go | 27 ---------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 0a724cfe948..504e7d04382 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -290,7 +290,7 @@ func (l *SketchLibrariesDetector) findIncludes( if !l.useCachedLibrariesResolution { sketch := sketch - mergedfile, err := makeSourceFile(sketchBuildPath, sketchBuildPath, paths.New(sketch.MainFile.Base()+".cpp")) + mergedfile, err := l.makeSourceFile(sketchBuildPath, sketchBuildPath, paths.New(sketch.MainFile.Base()+".cpp")) if err != nil { return err } @@ -503,7 +503,7 @@ func (l *SketchLibrariesDetector) queueSourceFilesFromFolder( } for _, filePath := range filePaths { - sourceFile, err := makeSourceFile(sourceDir, buildDir, filePath, extraIncludePath...) + sourceFile, err := l.makeSourceFile(sourceDir, buildDir, filePath, extraIncludePath...) if err != nil { return err } @@ -513,6 +513,33 @@ func (l *SketchLibrariesDetector) queueSourceFilesFromFolder( return nil } +// makeSourceFile create a sourceFile object for the given source file path. +// The given sourceFilePath can be absolute, or relative within the sourceRoot root folder. +func (l *SketchLibrariesDetector) makeSourceFile(sourceRoot, buildRoot, sourceFilePath *paths.Path, extraIncludePaths ...*paths.Path) (*sourceFile, error) { + if len(extraIncludePaths) > 1 { + panic("only one extra include path allowed") + } + var extraIncludePath *paths.Path + if len(extraIncludePaths) > 0 { + extraIncludePath = extraIncludePaths[0] + } + + if sourceFilePath.IsAbs() { + var err error + sourceFilePath, err = sourceRoot.RelTo(sourceFilePath) + if err != nil { + return nil, err + } + } + res := &sourceFile{ + SourcePath: sourceRoot.JoinPath(sourceFilePath), + ObjectPath: buildRoot.Join(sourceFilePath.String() + ".o"), + DepfilePath: buildRoot.Join(sourceFilePath.String() + ".d"), + ExtraIncludePath: extraIncludePath, + } + return res, nil +} + func (l *SketchLibrariesDetector) failIfImportedLibraryIsWrong() error { if len(l.importedLibraries) == 0 { return nil diff --git a/internal/arduino/builder/internal/detector/source_file.go b/internal/arduino/builder/internal/detector/source_file.go index d99cb1d862a..c37190bbc5e 100644 --- a/internal/arduino/builder/internal/detector/source_file.go +++ b/internal/arduino/builder/internal/detector/source_file.go @@ -54,33 +54,6 @@ func (f *sourceFile) Equals(g *sourceFile) bool { (f.ExtraIncludePath != nil && g.ExtraIncludePath != nil && f.ExtraIncludePath.EqualsTo(g.ExtraIncludePath))) } -// makeSourceFile create a sourceFile object for the given source file path. -// The given sourceFilePath can be absolute, or relative within the sourceRoot root folder. -func makeSourceFile(sourceRoot, buildRoot, sourceFilePath *paths.Path, extraIncludePaths ...*paths.Path) (*sourceFile, error) { - if len(extraIncludePaths) > 1 { - panic("only one extra include path allowed") - } - var extraIncludePath *paths.Path - if len(extraIncludePaths) > 0 { - extraIncludePath = extraIncludePaths[0] - } - - if sourceFilePath.IsAbs() { - var err error - sourceFilePath, err = sourceRoot.RelTo(sourceFilePath) - if err != nil { - return nil, err - } - } - res := &sourceFile{ - SourcePath: sourceRoot.JoinPath(sourceFilePath), - ObjectPath: buildRoot.Join(sourceFilePath.String() + ".o"), - DepfilePath: buildRoot.Join(sourceFilePath.String() + ".d"), - ExtraIncludePath: extraIncludePath, - } - return res, nil -} - // ObjFileIsUpToDate checks if the compile object file is up to date. func (f *sourceFile) ObjFileIsUpToDate() (unchanged bool, err error) { return utils.ObjFileIsUpToDate(f.SourcePath, f.ObjectPath, f.DepfilePath) From 7c084fa40c8189393d7c5df57e2dbfba7fd58dc5 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 25 Aug 2025 16:06:48 +0200 Subject: [PATCH 02/13] Clone list before changing it by append --- internal/arduino/builder/internal/detector/detector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 504e7d04382..15e26708e94 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -355,7 +355,7 @@ func (l *SketchLibrariesDetector) gccPreprocessTask(sourceFile *sourceFile, buil // search path, but only for the source code of the library, so we temporary // copy the current search path list and add the library' utility directory // if needed. - includeFolders := l.includeFolders + includeFolders := l.includeFolders.Clone() if extraInclude := sourceFile.ExtraIncludePath; extraInclude != nil { includeFolders = append(includeFolders, extraInclude) } From e6dc1aa718b91729c36ad790a18d19c60057b507 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 26 Aug 2025 13:04:41 +0200 Subject: [PATCH 03/13] Refactored integration test --- .../compile_4/lib_discovery_caching_test.go | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/internal/integrationtest/compile_4/lib_discovery_caching_test.go b/internal/integrationtest/compile_4/lib_discovery_caching_test.go index 9d8bd4b5277..90a1ccc3d61 100644 --- a/internal/integrationtest/compile_4/lib_discovery_caching_test.go +++ b/internal/integrationtest/compile_4/lib_discovery_caching_test.go @@ -39,72 +39,69 @@ func TestLibDiscoveryCache(t *testing.T) { require.NoError(t, sketchbook.RemoveAll()) require.NoError(t, testdata.CopyDirTo(cli.SketchbookDir())) - buildpath, err := paths.MkTempDir("", "tmpbuildpath") - require.NoError(t, err) - t.Cleanup(func() { buildpath.RemoveAll() }) - - { + t.Run("BasicLibDiscovery", func(t *testing.T) { sketchA := sketchbook.Join("SketchA") + buildpath, err := sketchA.Join("build").Abs() + require.NoError(t, err) + t.Cleanup(func() { buildpath.RemoveAll() }) + { + require.NoError(t, sketchA.Join("SketchA.ino").WriteFile([]byte(` +#include +void setup() {} +void loop() {libAFunction();}`))) outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) require.NoError(t, err) j := requirejson.Parse(t, outjson) - j.MustContain(`{"builder_result":{ - "used_libraries": [ - { "name": "LibA" }, - { "name": "LibB" } - ], - }}`) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["LibA", "LibB"]`) } - // Update SketchA - require.NoError(t, sketchA.Join("SketchA.ino").WriteFile([]byte(` + { + // Update SketchA + require.NoError(t, sketchA.Join("SketchA.ino").WriteFile([]byte(` #include #include void setup() {} void loop() {libAFunction();} -`))) - - { + `))) // This compile should FAIL! outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) require.Error(t, err) j := requirejson.Parse(t, outjson) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["LibC", "LibA"]`) j.MustContain(`{ -"builder_result":{ - "used_libraries": [ - { "name": "LibC" }, - { "name": "LibA" } - ], - "diagnostics": [ - { - "severity": "ERROR", - "message": "'libAFunction' was not declared in this scope\n void loop() {libAFunction();}\n ^~~~~~~~~~~~" - } - ] -}}`) + "builder_result":{ + "diagnostics": [ + { + "severity": "ERROR", + "message": "'libAFunction' was not declared in this scope\n void loop() {libAFunction();}\n ^~~~~~~~~~~~" + } + ] + }}`) j.Query(".compiler_out").MustContain(`"The list of included libraries has been changed... rebuilding all libraries."`) } { + // Compile again the bad sketch + // This compile should FAIL! outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) require.Error(t, err) j := requirejson.Parse(t, outjson) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["LibC", "LibA"]`) j.MustContain(`{ -"builder_result":{ - "used_libraries": [ - { "name": "LibC" }, - { "name": "LibA" } - ], - "diagnostics": [ - { - "severity": "ERROR", - "message": "'libAFunction' was not declared in this scope\n void loop() {libAFunction();}\n ^~~~~~~~~~~~" - } - ] -}}`) + "builder_result":{ + "diagnostics": [ + { + "severity": "ERROR", + "message": "'libAFunction' was not declared in this scope\n void loop() {libAFunction();}\n ^~~~~~~~~~~~" + } + ] + }}`) j.Query(".compiler_out").MustNotContain(`"The list of included libraries has changed... rebuilding all libraries."`) } - } + }) } From a3123d12019177136b81bfb82b0a1a04d6d0da5f Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 10 Oct 2025 17:05:16 +0200 Subject: [PATCH 04/13] CI: Set process I/O streams before starting copying. --- internal/integrationtest/arduino-cli.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go index c990b24c6ae..b8f645cc58c 100644 --- a/internal/integrationtest/arduino-cli.go +++ b/internal/integrationtest/arduino-cli.go @@ -389,22 +389,22 @@ func (cli *ArduinoCLI) run(ctx context.Context, stdoutBuff, stderrBuff io.Writer cli.t.NoError(cliProc.Start()) + if stdoutBuff == nil { + stdoutBuff = io.Discard + } + if stderrBuff == nil { + stderrBuff = io.Discard + } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() - if stdoutBuff == nil { - stdoutBuff = io.Discard - } if _, err := io.Copy(stdoutBuff, io.TeeReader(stdout, terminalOut)); err != nil { fmt.Fprintln(terminalOut, color.HiBlackString("<<< stdout copy error:"), err) } }() go func() { defer wg.Done() - if stderrBuff == nil { - stderrBuff = io.Discard - } if _, err := io.Copy(stderrBuff, io.TeeReader(stderr, terminalErr)); err != nil { fmt.Fprintln(terminalErr, color.HiBlackString("<<< stderr copy error:"), err) } From 8f82b6d7a17ad11ee44db3d268f66a610183c40d Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Jul 2025 12:15:09 +0200 Subject: [PATCH 05/13] Avoid rebuilding the sketch if the sketch file is unchanged. --- .../builder/internal/preprocessor/ctags.go | 20 ++++++++++++++----- internal/arduino/builder/sketch.go | 15 ++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/internal/arduino/builder/internal/preprocessor/ctags.go b/internal/arduino/builder/internal/preprocessor/ctags.go index f66fbabf5cf..c8801aeb3df 100644 --- a/internal/arduino/builder/internal/preprocessor/ctags.go +++ b/internal/arduino/builder/internal/preprocessor/ctags.go @@ -55,9 +55,19 @@ func PreprocessSketchWithCtags( stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} + // Check if the preprocessed file is already up-to-date + unpreprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp.merged") + preprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp") + if unpreprocessedStat, err := unpreprocessedSourceFile.Stat(); err != nil { + return nil, fmt.Errorf("%s: %w", i18n.Tr("unable to open unpreprocessed source file"), err) + } else if sourceStat, err := preprocessedSourceFile.Stat(); err == nil && unpreprocessedStat.ModTime().Before(sourceStat.ModTime()) { + fmt.Fprintln(stdout, i18n.Tr("Sketch is unchanged, skipping preprocessing.")) + res := &runner.Result{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} + return res, nil + } + // Run GCC preprocessor - sourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp") - result := GCC(sourceFile, ctagsTarget, includes, buildProperties).Run(ctx) + result := GCC(unpreprocessedSourceFile, ctagsTarget, includes, buildProperties).Run(ctx) stdout.Write(result.Stdout) stderr.Write(result.Stderr) if err := result.Error; err != nil { @@ -69,7 +79,7 @@ func PreprocessSketchWithCtags( fmt.Fprintf(stderr, "%s: %s", i18n.Tr("An error occurred adding prototypes"), i18n.Tr("the compilation database may be incomplete or inaccurate")) - if err := sourceFile.CopyTo(ctagsTarget); err != nil { + if err := unpreprocessedSourceFile.CopyTo(ctagsTarget); err != nil { return &runner.Result{Args: result.Args, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, err } } @@ -102,7 +112,7 @@ func PreprocessSketchWithCtags( // Add prototypes to the original sketch source var source string - if sourceData, err := sourceFile.ReadFile(); err == nil { + if sourceData, err := unpreprocessedSourceFile.ReadFile(); err == nil { source = string(sourceData) } else { return &runner.Result{Args: result.Args, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, err @@ -136,7 +146,7 @@ func PreprocessSketchWithCtags( } // Write back arduino-preprocess output to the sourceFile - err = sourceFile.WriteFile([]byte(preprocessedSource)) + err = preprocessedSourceFile.WriteFile([]byte(preprocessedSource)) return &runner.Result{Args: result.Args, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, err } diff --git a/internal/arduino/builder/sketch.go b/internal/arduino/builder/sketch.go index f5196630a9b..cf25afa8abd 100644 --- a/internal/arduino/builder/sketch.go +++ b/internal/arduino/builder/sketch.go @@ -58,9 +58,20 @@ func (b *Builder) prepareSketchBuildPath() error { return err } + // Save the unpreprocessed merged source to a file named sketch.cpp.merged. + destFileUnpreprocessed := b.sketchBuildPath.Join(b.sketch.MainFile.Base() + ".cpp.merged") destFile := b.sketchBuildPath.Join(b.sketch.MainFile.Base() + ".cpp") - if err := destFile.WriteFile([]byte(mergedSource)); err != nil { - return err + oldUnpreprocessedSource, _ := destFileUnpreprocessed.ReadFile() + + // If the merged source is unchanged, skip writing it. + // This avoids unnecessary rebuilds and keeps the build path clean. + if !bytes.Equal(oldUnpreprocessedSource, []byte(mergedSource)) { + if err := destFileUnpreprocessed.WriteFile([]byte(mergedSource)); err != nil { + return err + } + if err := destFile.WriteFile([]byte(mergedSource)); err != nil { + return err + } } if err := b.sketchCopyAdditionalFiles(b.sketchBuildPath, b.sourceOverrides); err != nil { From 3d7129770d10fda15d5a2e1130c2ed158c6f27ca Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 25 Aug 2025 16:14:09 +0200 Subject: [PATCH 06/13] Allow dep-file generation in GCC preprocessor --- internal/arduino/builder/internal/detector/detector.go | 2 +- .../builder/internal/preprocessor/arduino_preprocessor.go | 2 +- internal/arduino/builder/internal/preprocessor/ctags.go | 2 +- internal/arduino/builder/internal/preprocessor/gcc.go | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 15e26708e94..ca74e2d776d 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -360,7 +360,7 @@ func (l *SketchLibrariesDetector) gccPreprocessTask(sourceFile *sourceFile, buil includeFolders = append(includeFolders, extraInclude) } - return preprocessor.GCC(sourceFile.SourcePath, paths.NullPath(), includeFolders, buildProperties) + return preprocessor.GCC(sourceFile.SourcePath, paths.NullPath(), includeFolders, buildProperties, nil) } func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( diff --git a/internal/arduino/builder/internal/preprocessor/arduino_preprocessor.go b/internal/arduino/builder/internal/preprocessor/arduino_preprocessor.go index dd847c26eda..250cf7ea3ba 100644 --- a/internal/arduino/builder/internal/preprocessor/arduino_preprocessor.go +++ b/internal/arduino/builder/internal/preprocessor/arduino_preprocessor.go @@ -45,7 +45,7 @@ func PreprocessSketchWithArduinoPreprocessor( sourceFile := buildPath.Join("sketch", sk.MainFile.Base()+".cpp") targetFile := buildPath.Join("preproc", "sketch_merged.cpp") - gccResult := GCC(sourceFile, targetFile, includeFolders, buildProperties).Run(ctx) + gccResult := GCC(sourceFile, targetFile, includeFolders, buildProperties, nil).Run(ctx) verboseOut.Write(gccResult.Stdout) verboseOut.Write(gccResult.Stderr) if gccResult.Error != nil { diff --git a/internal/arduino/builder/internal/preprocessor/ctags.go b/internal/arduino/builder/internal/preprocessor/ctags.go index c8801aeb3df..54f82a2bbef 100644 --- a/internal/arduino/builder/internal/preprocessor/ctags.go +++ b/internal/arduino/builder/internal/preprocessor/ctags.go @@ -67,7 +67,7 @@ func PreprocessSketchWithCtags( } // Run GCC preprocessor - result := GCC(unpreprocessedSourceFile, ctagsTarget, includes, buildProperties).Run(ctx) + result := GCC(unpreprocessedSourceFile, ctagsTarget, includes, buildProperties, nil).Run(ctx) stdout.Write(result.Stdout) stderr.Write(result.Stderr) if err := result.Error; err != nil { diff --git a/internal/arduino/builder/internal/preprocessor/gcc.go b/internal/arduino/builder/internal/preprocessor/gcc.go index 31dc2922ab7..6baf2d8391f 100644 --- a/internal/arduino/builder/internal/preprocessor/gcc.go +++ b/internal/arduino/builder/internal/preprocessor/gcc.go @@ -30,6 +30,7 @@ import ( func GCC( sourceFilePath, targetFilePath *paths.Path, includes paths.PathList, buildProperties *properties.Map, + depFile *paths.Path, ) *runner.Task { gccBuildProperties := properties.NewMap() gccBuildProperties.Set("preproc.macros.flags", "-w -x c++ -E -CC") @@ -62,6 +63,11 @@ func GCC( // to create a /dev/null.d dependency file, which won't work. args = f.Filter(args, f.NotEquals("-MMD")) + // If a depFile has been specified add the necessary arguments to generate it + if depFile != nil { + args = append(args, "-MMD", "-MF", depFile.String()) + } + // Limit the stderr output to 100 KiB // https://github.com/arduino/arduino-cli/pull/2883 return runner.NewTaskWithLimitedStderr(100*1024, args...) From 184f50823c364f35ee5b97082faa664d96056d03 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 26 Aug 2025 15:07:42 +0200 Subject: [PATCH 07/13] Added integration test for lib-discovery-caching --- .../compile_4/lib_discovery_caching_test.go | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/internal/integrationtest/compile_4/lib_discovery_caching_test.go b/internal/integrationtest/compile_4/lib_discovery_caching_test.go index 90a1ccc3d61..954ab148e83 100644 --- a/internal/integrationtest/compile_4/lib_discovery_caching_test.go +++ b/internal/integrationtest/compile_4/lib_discovery_caching_test.go @@ -28,10 +28,6 @@ func TestLibDiscoveryCache(t *testing.T) { env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t) t.Cleanup(env.CleanUp) - // Install Arduino AVR Boards - _, _, err := cli.Run("core", "install", "arduino:avr@1.8.6") - require.NoError(t, err) - // Copy the testdata sketchbook testdata, err := paths.New("testdata", "libraries_discovery_caching").Abs() require.NoError(t, err) @@ -39,7 +35,59 @@ func TestLibDiscoveryCache(t *testing.T) { require.NoError(t, sketchbook.RemoveAll()) require.NoError(t, testdata.CopyDirTo(cli.SketchbookDir())) - t.Run("BasicLibDiscovery", func(t *testing.T) { + // Install Arduino AVR Boards + _, _, err = cli.Run("core", "install", "arduino:avr@1.8.6") + require.NoError(t, err) + // Install Ethernet library + _, _, err = cli.Run("lib", "install", "Ethernet") + require.NoError(t, err) + + t.Run("RemoveLibWithoutError", func(t *testing.T) { + sketchA := sketchbook.Join("SketchA") + buildpath, err := sketchA.Join("build").Abs() + require.NoError(t, err) + t.Cleanup(func() { buildpath.RemoveAll() }) + + { + require.NoError(t, sketchA.Join("SketchA.ino").WriteFile([]byte(` +#include +#include +void setup() {} +void loop() {}`))) + outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) + require.NoError(t, err) + j := requirejson.Parse(t, outjson) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["SPI", "Ethernet"]`) + } + + { + // Update SketchA + require.NoError(t, sketchA.Join("SketchA.ino").WriteFile([]byte(` +#include +void setup() {} +void loop() {}`))) + // This compile should not include Ethernet + outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) + require.NoError(t, err) + j := requirejson.Parse(t, outjson) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["SPI"]`) + j.Query(".compiler_out").MustContain(`"The list of included libraries has been changed... rebuilding all libraries."`) + } + + { + // This compile should not rebuild libs + outjson, _, err := cli.Run("compile", "-v", "-b", "arduino:avr:uno", "--build-path", buildpath.String(), "--json", sketchA.String()) + require.NoError(t, err) + j := requirejson.Parse(t, outjson) + usedLibs := j.Query("[.builder_result.used_libraries[].name]") + usedLibs.MustEqual(`["SPI"]`) + j.Query(".compiler_out").MustNotContain(`"The list of included libraries has changed... rebuilding all libraries."`) + } + }) + + t.Run("RemoveLibWithError", func(t *testing.T) { sketchA := sketchbook.Join("SketchA") buildpath, err := sketchA.Join("build").Abs() require.NoError(t, err) From 33ff6f431d9ebed409018c1aea100af514199980 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 2 Sep 2025 17:56:23 +0200 Subject: [PATCH 08/13] Fixed ObjFileIsUptodate checks in library discovery. During the library detection phase, we do not have objects files yet, the only thing that we should check is if the dependency file is up-to-date. In this commit we let GCC create a dep file during the library discovery phase. We must ensure that the build-path folder exists otherwise the compiler will not be able to create the dep file. --- .../builder/internal/detector/detector.go | 7 +- .../builder/internal/detector/source_file.go | 76 +++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index ca74e2d776d..86917e79e38 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -275,6 +275,7 @@ func (l *SketchLibrariesDetector) findIncludes( if entry.Compile != nil && entry.CompileTask != nil { upToDate, _ := entry.Compile.ObjFileIsUpToDate() if !upToDate { + _ = entry.Compile.PrepareBuildPath() l.preRunner.Enqueue(entry.CompileTask) } } @@ -360,7 +361,8 @@ func (l *SketchLibrariesDetector) gccPreprocessTask(sourceFile *sourceFile, buil includeFolders = append(includeFolders, extraInclude) } - return preprocessor.GCC(sourceFile.SourcePath, paths.NullPath(), includeFolders, buildProperties, nil) + _ = sourceFile.PrepareBuildPath() + return preprocessor.GCC(sourceFile.SourcePath, paths.NullPath(), includeFolders, buildProperties, sourceFile.DepfilePath) } func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( @@ -533,8 +535,7 @@ func (l *SketchLibrariesDetector) makeSourceFile(sourceRoot, buildRoot, sourceFi } res := &sourceFile{ SourcePath: sourceRoot.JoinPath(sourceFilePath), - ObjectPath: buildRoot.Join(sourceFilePath.String() + ".o"), - DepfilePath: buildRoot.Join(sourceFilePath.String() + ".d"), + DepfilePath: buildRoot.Join(fmt.Sprintf("%s.libsdetect.d", sourceFilePath)), ExtraIncludePath: extraIncludePath, } return res, nil diff --git a/internal/arduino/builder/internal/detector/source_file.go b/internal/arduino/builder/internal/detector/source_file.go index c37190bbc5e..51a8ac1d8da 100644 --- a/internal/arduino/builder/internal/detector/source_file.go +++ b/internal/arduino/builder/internal/detector/source_file.go @@ -19,17 +19,17 @@ import ( "fmt" "slices" - "github.com/arduino/arduino-cli/internal/arduino/builder/internal/utils" + "os" + + "github.com/arduino/arduino-cli/internal/arduino/builder/cpp" "github.com/arduino/go-paths-helper" + "github.com/sirupsen/logrus" ) type sourceFile struct { // SourcePath is the path to the source file SourcePath *paths.Path `json:"source_path"` - // ObjectPath is the path to the object file that will be generated - ObjectPath *paths.Path `json:"object_path"` - // DepfilePath is the path to the dependency file that will be generated DepfilePath *paths.Path `json:"depfile_path"` @@ -41,22 +41,82 @@ type sourceFile struct { } func (f *sourceFile) String() string { - return fmt.Sprintf("SourcePath:%s SourceRoot:%s BuildRoot:%s ExtraInclude:%s", - f.SourcePath, f.ObjectPath, f.DepfilePath, f.ExtraIncludePath) + return fmt.Sprintf("%s -> dep:%s (ExtraInclude:%s)", + f.SourcePath, f.DepfilePath, f.ExtraIncludePath) } // Equals checks if a sourceFile is equal to another. func (f *sourceFile) Equals(g *sourceFile) bool { return f.SourcePath.EqualsTo(g.SourcePath) && - f.ObjectPath.EqualsTo(g.ObjectPath) && f.DepfilePath.EqualsTo(g.DepfilePath) && ((f.ExtraIncludePath == nil && g.ExtraIncludePath == nil) || (f.ExtraIncludePath != nil && g.ExtraIncludePath != nil && f.ExtraIncludePath.EqualsTo(g.ExtraIncludePath))) } +// PrepareBuildPath ensures that the directory for the dependency file exists. +func (f *sourceFile) PrepareBuildPath() error { + if f.DepfilePath != nil { + return f.DepfilePath.Parent().MkdirAll() + } + return nil +} + // ObjFileIsUpToDate checks if the compile object file is up to date. func (f *sourceFile) ObjFileIsUpToDate() (unchanged bool, err error) { - return utils.ObjFileIsUpToDate(f.SourcePath, f.ObjectPath, f.DepfilePath) + logrus.Debugf("Checking previous results for %v (dep = %v)", f.SourcePath, f.DepfilePath) + if f.DepfilePath == nil { + logrus.Debugf(" Object file or dependency file not provided") + return false, nil + } + + sourceFile := f.SourcePath.Clean() + sourceFileStat, err := sourceFile.Stat() + if err != nil { + logrus.Debugf(" Could not stat source file: %s", err) + return false, err + } + dependencyFile := f.DepfilePath.Clean() + dependencyFileStat, err := dependencyFile.Stat() + if err != nil { + if os.IsNotExist(err) { + logrus.Debugf(" Dependency file not found: %v", dependencyFile) + return false, nil + } + logrus.Debugf(" Could not stat dependency file: %s", err) + return false, err + } + if sourceFileStat.ModTime().After(dependencyFileStat.ModTime()) { + logrus.Debugf(" %v newer than %v", sourceFile, dependencyFile) + return false, nil + } + deps, err := cpp.ReadDepFile(dependencyFile) + if err != nil { + logrus.Debugf(" Could not read dependency file: %s", dependencyFile) + return false, err + } + if len(deps.Dependencies) == 0 { + return true, nil + } + if deps.Dependencies[0] != sourceFile.String() { + logrus.Debugf(" Depfile is about different source file: %v (expected %v)", deps.Dependencies[0], sourceFile) + return false, nil + } + for _, dep := range deps.Dependencies[1:] { + depStat, err := os.Stat(dep) + if os.IsNotExist(err) { + logrus.Debugf(" Not found: %v", dep) + return false, nil + } + if err != nil { + logrus.WithError(err).Debugf(" Failed to read: %v", dep) + return false, nil + } + if depStat.ModTime().After(dependencyFileStat.ModTime()) { + logrus.Debugf(" %v newer than %v", dep, dependencyFile) + return false, nil + } + } + return true, nil } // uniqueSourceFileQueue is a queue of source files that does not allow duplicates. From 21b3ee518f5afca93c1c0b6e9b4cfd7eaca356ef Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 2 Sep 2025 17:47:14 +0200 Subject: [PATCH 09/13] Structure sourceFile is now copied instead of passed-by-pointer --- internal/arduino/builder/internal/detector/cache.go | 4 ++-- .../arduino/builder/internal/detector/detector.go | 13 ++++++------- .../builder/internal/detector/source_file.go | 10 +++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/internal/arduino/builder/internal/detector/cache.go b/internal/arduino/builder/internal/detector/cache.go index 62b3d355678..4cfd212e384 100644 --- a/internal/arduino/builder/internal/detector/cache.go +++ b/internal/arduino/builder/internal/detector/cache.go @@ -30,7 +30,7 @@ type detectorCache struct { type detectorCacheEntry struct { AddedIncludePath *paths.Path `json:"added_include_path,omitempty"` - Compile *sourceFile `json:"compile,omitempty"` + Compile sourceFile `json:"compile,omitempty"` CompileTask *runner.Task `json:"compile_task,omitempty"` MissingIncludeH *string `json:"missing_include_h,omitempty"` } @@ -39,7 +39,7 @@ func (e *detectorCacheEntry) String() string { if e.AddedIncludePath != nil { return "Added include path: " + e.AddedIncludePath.String() } - if e.Compile != nil && e.CompileTask != nil { + if e.CompileTask != nil { return "Compiling: " + e.Compile.String() + " / " + e.CompileTask.String() } if e.MissingIncludeH != nil { diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 86917e79e38..06a6ecc40b3 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -272,7 +272,7 @@ func (l *SketchLibrariesDetector) findIncludes( // Pre-run cache entries l.preRunner = runner.New(ctx, jobs) for _, entry := range l.cache.EntriesAhead() { - if entry.Compile != nil && entry.CompileTask != nil { + if entry.CompileTask != nil { upToDate, _ := entry.Compile.ObjFileIsUpToDate() if !upToDate { _ = entry.Compile.PrepareBuildPath() @@ -351,7 +351,7 @@ func (l *SketchLibrariesDetector) findIncludes( return nil } -func (l *SketchLibrariesDetector) gccPreprocessTask(sourceFile *sourceFile, buildProperties *properties.Map) *runner.Task { +func (l *SketchLibrariesDetector) gccPreprocessTask(sourceFile sourceFile, buildProperties *properties.Map) *runner.Task { // Libraries may require the "utility" directory to be added to the include // search path, but only for the source code of the library, so we temporary // copy the current search path list and add the library' utility directory @@ -517,7 +517,7 @@ func (l *SketchLibrariesDetector) queueSourceFilesFromFolder( // makeSourceFile create a sourceFile object for the given source file path. // The given sourceFilePath can be absolute, or relative within the sourceRoot root folder. -func (l *SketchLibrariesDetector) makeSourceFile(sourceRoot, buildRoot, sourceFilePath *paths.Path, extraIncludePaths ...*paths.Path) (*sourceFile, error) { +func (l *SketchLibrariesDetector) makeSourceFile(sourceRoot, buildRoot, sourceFilePath *paths.Path, extraIncludePaths ...*paths.Path) (sourceFile, error) { if len(extraIncludePaths) > 1 { panic("only one extra include path allowed") } @@ -530,15 +530,14 @@ func (l *SketchLibrariesDetector) makeSourceFile(sourceRoot, buildRoot, sourceFi var err error sourceFilePath, err = sourceRoot.RelTo(sourceFilePath) if err != nil { - return nil, err + return sourceFile{}, err } } - res := &sourceFile{ + return sourceFile{ SourcePath: sourceRoot.JoinPath(sourceFilePath), DepfilePath: buildRoot.Join(fmt.Sprintf("%s.libsdetect.d", sourceFilePath)), ExtraIncludePath: extraIncludePath, - } - return res, nil + }, nil } func (l *SketchLibrariesDetector) failIfImportedLibraryIsWrong() error { diff --git a/internal/arduino/builder/internal/detector/source_file.go b/internal/arduino/builder/internal/detector/source_file.go index 51a8ac1d8da..054776dafcf 100644 --- a/internal/arduino/builder/internal/detector/source_file.go +++ b/internal/arduino/builder/internal/detector/source_file.go @@ -46,7 +46,7 @@ func (f *sourceFile) String() string { } // Equals checks if a sourceFile is equal to another. -func (f *sourceFile) Equals(g *sourceFile) bool { +func (f *sourceFile) Equals(g sourceFile) bool { return f.SourcePath.EqualsTo(g.SourcePath) && f.DepfilePath.EqualsTo(g.DepfilePath) && ((f.ExtraIncludePath == nil && g.ExtraIncludePath == nil) || @@ -120,22 +120,22 @@ func (f *sourceFile) ObjFileIsUpToDate() (unchanged bool, err error) { } // uniqueSourceFileQueue is a queue of source files that does not allow duplicates. -type uniqueSourceFileQueue []*sourceFile +type uniqueSourceFileQueue []sourceFile // Push adds a source file to the queue if it is not already present. -func (queue *uniqueSourceFileQueue) Push(value *sourceFile) { +func (queue *uniqueSourceFileQueue) Push(value sourceFile) { if !queue.Contains(value) { *queue = append(*queue, value) } } // Contains checks if the queue Contains a source file. -func (queue uniqueSourceFileQueue) Contains(target *sourceFile) bool { +func (queue uniqueSourceFileQueue) Contains(target sourceFile) bool { return slices.ContainsFunc(queue, target.Equals) } // Pop removes and returns the first element of the queue. -func (queue *uniqueSourceFileQueue) Pop() *sourceFile { +func (queue *uniqueSourceFileQueue) Pop() sourceFile { old := *queue x := old[0] *queue = old[1:] From 237f0ccb84516c612ffbbe708f2fe1c6d051c50f Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 2 Sep 2025 18:00:41 +0200 Subject: [PATCH 10/13] Tracing for Library Discovery --- .../builder/internal/detector/cache.go | 13 ++++++++++ .../builder/internal/detector/detector.go | 17 +++++++++++-- .../builder/internal/detector/source_file.go | 25 ++++++++++--------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/internal/arduino/builder/internal/detector/cache.go b/internal/arduino/builder/internal/detector/cache.go index 4cfd212e384..6db636fb39b 100644 --- a/internal/arduino/builder/internal/detector/cache.go +++ b/internal/arduino/builder/internal/detector/cache.go @@ -21,6 +21,7 @@ import ( "github.com/arduino/arduino-cli/internal/arduino/builder/internal/runner" "github.com/arduino/go-paths-helper" + "github.com/sirupsen/logrus" ) type detectorCache struct { @@ -51,6 +52,13 @@ func (e *detectorCacheEntry) String() string { return "No operation" } +func (e *detectorCacheEntry) LogMsg() string { + if e.CompileTask == nil { + return e.String() + } + return "Compiling: " + e.Compile.SourcePath.String() +} + func (e *detectorCacheEntry) Equals(entry *detectorCacheEntry) bool { return e.String() == entry.String() } @@ -94,10 +102,15 @@ func (c *detectorCache) Expect(entry *detectorCacheEntry) { if c.entries[c.curr].Equals(entry) { // Cache hit, move to the next entry c.curr++ + logrus.Tracef("[LD] CACHE: HIT %s", entry.LogMsg()) return } // Cache mismatch, invalidate and cut the remainder of the cache + logrus.Tracef("[LD] CACHE: INVALIDATE %s", entry.LogMsg()) + logrus.Tracef("[LD] (was %s)", c.entries[c.curr]) c.entries = c.entries[:c.curr] + } else { + logrus.Tracef("[LD] CACHE: MISSING %s", entry.LogMsg()) } c.curr++ c.entries = append(c.entries, entry) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 06a6ecc40b3..5b3740be682 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -41,6 +41,7 @@ import ( "github.com/arduino/arduino-cli/internal/i18n" "github.com/arduino/go-paths-helper" "github.com/arduino/go-properties-orderedmap" + "github.com/sirupsen/logrus" ) type libraryResolutionResult struct { @@ -140,6 +141,7 @@ func (l *SketchLibrariesDetector) ImportedLibraries() libraries.List { // addAndBuildLibrary adds the given library to the imported libraries list and queues its source files // for further processing. func (l *SketchLibrariesDetector) addAndBuildLibrary(sourceFileQueue *uniqueSourceFileQueue, librariesBuildPath *paths.Path, library *libraries.Library) { + logrus.Tracef("[LD] LIBRARY: %s", library.Name) l.importedLibraries = append(l.importedLibraries, library) if library.Precompiled && library.PrecompiledWithSources { // Fully precompiled libraries should have no dependencies to avoid ABI breakage @@ -202,6 +204,7 @@ func (l *SketchLibrariesDetector) IncludeFoldersChanged() bool { // addIncludeFolder add the given folder to the include path. func (l *SketchLibrariesDetector) addIncludeFolder(folder *paths.Path) { + logrus.Tracef("[LD] INCLUDE-PATH: %s", folder.String()) l.includeFolders = append(l.includeFolders, folder) l.cache.Expect(&detectorCacheEntry{AddedIncludePath: folder}) } @@ -219,6 +222,11 @@ func (l *SketchLibrariesDetector) FindIncludes( platformArch string, jobs int, ) error { + logrus.Debug("Finding required libraries for the sketch.") + defer func() { + logrus.Debugf("Library detection completed. Found %d required libraries.", len(l.importedLibraries)) + }() + err := l.findIncludes(ctx, buildPath, buildCorePath, buildVariantPath, sketchBuildPath, sketch, librariesBuildPath, buildProperties, platformArch, jobs) if err != nil && l.onlyUpdateCompilationDatabase { l.logger.Info( @@ -273,7 +281,7 @@ func (l *SketchLibrariesDetector) findIncludes( l.preRunner = runner.New(ctx, jobs) for _, entry := range l.cache.EntriesAhead() { if entry.CompileTask != nil { - upToDate, _ := entry.Compile.ObjFileIsUpToDate() + upToDate, _ := entry.Compile.ObjFileIsUpToDate(logrus.WithField("runner", "prerun")) if !upToDate { _ = entry.Compile.PrepareBuildPath() l.preRunner.Enqueue(entry.CompileTask) @@ -387,7 +395,7 @@ func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( // TODO: This reads the dependency file, but the actual building // does it again. Should the result be somehow cached? Perhaps // remove the object file if it is found to be stale? - unchanged, err := sourceFile.ObjFileIsUpToDate() + unchanged, err := sourceFile.ObjFileIsUpToDate(logrus.WithField("runner", "main")) if err != nil { return err } @@ -403,11 +411,13 @@ func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( var missingIncludeH string if entry := l.cache.Peek(); unchanged && entry != nil && entry.MissingIncludeH != nil { missingIncludeH = *entry.MissingIncludeH + logrus.Tracef("[LD] COMPILE-CACHE: %s", sourceFile.SourcePath) if first && l.logger.VerbosityLevel() == logger.VerbosityVerbose { l.logger.Info(i18n.Tr("Using cached library dependencies for file: %[1]s", sourcePath)) } first = false } else { + logrus.Tracef("[LD] COMPILE: %s", sourceFile.SourcePath) if l.preRunner != nil { if r := l.preRunner.Results(preprocTask); r != nil { preprocResult = r @@ -448,6 +458,7 @@ func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( } } + logrus.Tracef("[LD] MISSING: %s", missingIncludeH) l.cache.Expect(&detectorCacheEntry{MissingIncludeH: &missingIncludeH}) if missingIncludeH == "" { @@ -495,6 +506,8 @@ func (l *SketchLibrariesDetector) queueSourceFilesFromFolder( buildDir *paths.Path, extraIncludePath ...*paths.Path, ) error { + logrus.Tracef("[LD] SCAN: %s (recurse=%v)", folder, recurse) + sourceFileExtensions := []string{} for k := range globals.SourceFilesValidExtensions { sourceFileExtensions = append(sourceFileExtensions, k) diff --git a/internal/arduino/builder/internal/detector/source_file.go b/internal/arduino/builder/internal/detector/source_file.go index 054776dafcf..088d3de46f6 100644 --- a/internal/arduino/builder/internal/detector/source_file.go +++ b/internal/arduino/builder/internal/detector/source_file.go @@ -62,60 +62,60 @@ func (f *sourceFile) PrepareBuildPath() error { } // ObjFileIsUpToDate checks if the compile object file is up to date. -func (f *sourceFile) ObjFileIsUpToDate() (unchanged bool, err error) { - logrus.Debugf("Checking previous results for %v (dep = %v)", f.SourcePath, f.DepfilePath) +func (f *sourceFile) ObjFileIsUpToDate(log *logrus.Entry) (unchanged bool, err error) { if f.DepfilePath == nil { - logrus.Debugf(" Object file or dependency file not provided") + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: object file or dependency file not provided", f.SourcePath) return false, nil } sourceFile := f.SourcePath.Clean() sourceFileStat, err := sourceFile.Stat() if err != nil { - logrus.Debugf(" Could not stat source file: %s", err) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Could not stat source file: %s", f.SourcePath, err) return false, err } dependencyFile := f.DepfilePath.Clean() dependencyFileStat, err := dependencyFile.Stat() if err != nil { if os.IsNotExist(err) { - logrus.Debugf(" Dependency file not found: %v", dependencyFile) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Dependency file not found: %v", f.SourcePath, dependencyFile) return false, nil } - logrus.Debugf(" Could not stat dependency file: %s", err) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Could not stat dependency file: %s", f.SourcePath, err) return false, err } if sourceFileStat.ModTime().After(dependencyFileStat.ModTime()) { - logrus.Debugf(" %v newer than %v", sourceFile, dependencyFile) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: %v newer than %v", f.SourcePath, sourceFile, dependencyFile) return false, nil } deps, err := cpp.ReadDepFile(dependencyFile) if err != nil { - logrus.Debugf(" Could not read dependency file: %s", dependencyFile) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Could not read dependency file: %s", f.SourcePath, dependencyFile) return false, err } if len(deps.Dependencies) == 0 { return true, nil } if deps.Dependencies[0] != sourceFile.String() { - logrus.Debugf(" Depfile is about different source file: %v (expected %v)", deps.Dependencies[0], sourceFile) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Depfile is about different source file: %v (expected %v)", f.SourcePath, deps.Dependencies[0], sourceFile) return false, nil } for _, dep := range deps.Dependencies[1:] { depStat, err := os.Stat(dep) if os.IsNotExist(err) { - logrus.Debugf(" Not found: %v", dep) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: Not found: %v", f.SourcePath, dep) return false, nil } if err != nil { - logrus.WithError(err).Debugf(" Failed to read: %v", dep) + logrus.WithError(err).Tracef("[LD] COMPILE-CHECK: REBUILD %v: Failed to read: %v", f.SourcePath, dep) return false, nil } if depStat.ModTime().After(dependencyFileStat.ModTime()) { - logrus.Debugf(" %v newer than %v", dep, dependencyFile) + log.Tracef("[LD] COMPILE-CHECK: REBUILD %v: %v newer than %v", f.SourcePath, dep, dependencyFile) return false, nil } } + log.Tracef("[LD] COMPILE-CHECK: REUSE %v Up-to-date", f.SourcePath) return true, nil } @@ -125,6 +125,7 @@ type uniqueSourceFileQueue []sourceFile // Push adds a source file to the queue if it is not already present. func (queue *uniqueSourceFileQueue) Push(value sourceFile) { if !queue.Contains(value) { + logrus.Tracef("[LD] QUEUE: Added %s", value.SourcePath) *queue = append(*queue, value) } } From b0c7c084cdefc404fe4a3958d5019a5822392075 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 2 Sep 2025 16:23:49 +0200 Subject: [PATCH 11/13] Build dir cleanup keeps lib-discovery output files --- internal/arduino/builder/builder.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/arduino/builder/builder.go b/internal/arduino/builder/builder.go index 1ae31ff31ee..e18dec2fed4 100644 --- a/internal/arduino/builder/builder.go +++ b/internal/arduino/builder/builder.go @@ -22,6 +22,7 @@ import ( "io" "os" "path/filepath" + "sort" "strings" "github.com/arduino/arduino-cli/internal/arduino/builder/internal/compilation" @@ -330,7 +331,7 @@ func (b *Builder) preprocess() error { if b.logger.VerbosityLevel() == logger.VerbosityVerbose { b.logger.Info(i18n.Tr("The list of included libraries has been changed... rebuilding all libraries.")) } - if err := b.librariesBuildPath.RemoveAll(); err != nil { + if err := b.removeBuildPathExecptLibsdiscoveryFiles(b.librariesBuildPath); err != nil { return err } } @@ -543,3 +544,28 @@ func (b *Builder) execCommand(command *paths.Process) error { return command.Wait() } + +func (b *Builder) removeBuildPathExecptLibsdiscoveryFiles(pathToRemove *paths.Path) error { + filesToRemove, err := pathToRemove.ReadDirRecursiveFiltered(nil, + paths.FilterOutDirectories(), + paths.FilterOutSuffixes(".libsdetect.d")) + if err != nil { + return err + } + for _, f := range filesToRemove { + if err := f.Remove(); err != nil { + return err + } + } + + dirsToRemove, err := pathToRemove.ReadDirRecursiveFiltered(nil, paths.FilterDirectories()) + if err != nil { + return err + } + // Remove directories in reverse order (deepest first) + sort.Slice(dirsToRemove, func(i, j int) bool { return len(dirsToRemove[i].String()) > len(dirsToRemove[j].String()) }) + for _, d := range dirsToRemove { + _ = d.Remove() + } + return nil +} From 68039314d277547973fe8f386ee95e9d7a2c03fb Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Thu, 9 Oct 2025 17:29:17 +0200 Subject: [PATCH 12/13] Ensure prerunner termination --- internal/arduino/builder/internal/detector/detector.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/arduino/builder/internal/detector/detector.go b/internal/arduino/builder/internal/detector/detector.go index 5b3740be682..59d5474a8b2 100644 --- a/internal/arduino/builder/internal/detector/detector.go +++ b/internal/arduino/builder/internal/detector/detector.go @@ -288,7 +288,11 @@ func (l *SketchLibrariesDetector) findIncludes( } } } - defer l.preRunner.Cancel() + defer func() { + if l.preRunner != nil { + l.preRunner.Cancel() + } + }() l.addIncludeFolder(buildCorePath) if buildVariantPath != nil { @@ -430,9 +434,8 @@ func (l *SketchLibrariesDetector) findMissingIncludesInCompilationUnit( // Stop the pre-runner if l.preRunner != nil { - preRunner := l.preRunner + l.preRunner.Cancel() l.preRunner = nil - go preRunner.Cancel() } // Run the actual preprocessor From 2d12579cc316174797f97f57b1eb35e650f4e99b Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 13 Oct 2025 09:30:58 +0200 Subject: [PATCH 13/13] Fixed checks for sketch preprocessor. --- .../builder/internal/preprocessor/ctags.go | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/arduino/builder/internal/preprocessor/ctags.go b/internal/arduino/builder/internal/preprocessor/ctags.go index 54f82a2bbef..07d90f83a3e 100644 --- a/internal/arduino/builder/internal/preprocessor/ctags.go +++ b/internal/arduino/builder/internal/preprocessor/ctags.go @@ -45,28 +45,19 @@ func PreprocessSketchWithCtags( lineOffset int, buildProperties *properties.Map, onlyUpdateCompilationDatabase, verbose bool, ) (*runner.Result, error) { + stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} + unpreprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp.merged") + preprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp") + // Create a temporary working directory tmpDir, err := paths.MkTempDir("", "") if err != nil { return nil, err } defer tmpDir.RemoveAll() - ctagsTarget := tmpDir.Join("sketch_merged.cpp") - - stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} - - // Check if the preprocessed file is already up-to-date - unpreprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp.merged") - preprocessedSourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp") - if unpreprocessedStat, err := unpreprocessedSourceFile.Stat(); err != nil { - return nil, fmt.Errorf("%s: %w", i18n.Tr("unable to open unpreprocessed source file"), err) - } else if sourceStat, err := preprocessedSourceFile.Stat(); err == nil && unpreprocessedStat.ModTime().Before(sourceStat.ModTime()) { - fmt.Fprintln(stdout, i18n.Tr("Sketch is unchanged, skipping preprocessing.")) - res := &runner.Result{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} - return res, nil - } // Run GCC preprocessor + ctagsTarget := tmpDir.Join("sketch_merged.cpp") result := GCC(unpreprocessedSourceFile, ctagsTarget, includes, buildProperties, nil).Run(ctx) stdout.Write(result.Stdout) stderr.Write(result.Stderr) @@ -145,6 +136,13 @@ func PreprocessSketchWithCtags( fmt.Println("#END OF PREPROCESSED SOURCE") } + // Read the existing preprocessed file to check if it's already up-to-date. + oldPreprocessedSource, _ := preprocessedSourceFile.ReadFile() + if bytes.Equal([]byte(preprocessedSource), oldPreprocessedSource) { + // No changes, do nothing + return &runner.Result{Args: result.Args, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil + } + // Write back arduino-preprocess output to the sourceFile err = preprocessedSourceFile.WriteFile([]byte(preprocessedSource)) return &runner.Result{Args: result.Args, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, err