diff --git a/legacy/builder/phases/linker.go b/legacy/builder/phases/linker.go index 0f2f76d336e..858a120bb8c 100644 --- a/legacy/builder/phases/linker.go +++ b/legacy/builder/phases/linker.go @@ -16,6 +16,8 @@ package phases import ( + "encoding/json" + "fmt" "strings" "github.com/arduino/arduino-cli/legacy/builder/builder_utils" @@ -63,46 +65,152 @@ func (s *Linker) Run(ctx *types.Context) error { return nil } +// CppArchive represents a cpp archive (.a). It has a list of object files +// that must be part of the archive and has functions to build the archive +// and check if the archive is up-to-date. +type CppArchive struct { + ArchivePath *paths.Path + CacheFilePath *paths.Path + Objects paths.PathList +} + +// NewCppArchive creates an empty CppArchive +func NewCppArchive(archivePath *paths.Path) *CppArchive { + return &CppArchive{ + ArchivePath: archivePath, + CacheFilePath: archivePath.Parent().Join(archivePath.Base() + ".cache"), + Objects: paths.NewPathList(), + } +} + +// AddObject adds an object file in the list of files to be archived +func (a *CppArchive) AddObject(object *paths.Path) { + a.Objects.Add(object) +} + +func (a *CppArchive) readCachedFilesList() paths.PathList { + var cache paths.PathList + if cacheData, err := a.CacheFilePath.ReadFile(); err != nil { + return nil + } else if err := json.Unmarshal(cacheData, &cache); err != nil { + return nil + } else { + return cache + } +} + +func (a *CppArchive) writeCachedFilesList() error { + if cacheData, err := json.Marshal(a.Objects); err != nil { + panic(err) // should never happen + } else if err := a.CacheFilePath.WriteFile(cacheData); err != nil { + return err + } else { + return nil + } +} + +// IsUpToDate checks if an already made archive is up-to-date. If this +// method returns true, there is no need to Create the archive. +func (a *CppArchive) IsUpToDate() bool { + archiveStat, err := a.ArchivePath.Stat() + if err != nil { + return false + } + + cache := a.readCachedFilesList() + if cache == nil || cache.Len() != a.Objects.Len() { + return false + } + for _, object := range cache { + objectStat, err := object.Stat() + if err != nil { + return false + } + if objectStat.ModTime().After(archiveStat.ModTime()) { + return false + } + } + + return true +} + +// Create will create the archive using the given arPattern +func (a *CppArchive) Create(ctx *types.Context, arPattern string) error { + _ = a.ArchivePath.Remove() + for _, object := range a.Objects { + properties := properties.NewMap() + properties.Set("archive_file", a.ArchivePath.Base()) + properties.SetPath("archive_file_path", a.ArchivePath) + properties.SetPath("object_file", object) + properties.Set("recipe.ar.pattern", arPattern) + command, err := builder_utils.PrepareCommandForRecipe(properties, "recipe.ar.pattern", false, ctx.PackageManager.GetEnvVarsForSpawnedProcess()) + if err != nil { + return errors.WithStack(err) + } + + if _, _, err := utils.ExecCommand(ctx, command, utils.ShowIfVerbose /* stdout */, utils.Show /* stderr */); err != nil { + return errors.WithStack(err) + } + } + + if err := a.writeCachedFilesList(); err != nil { + ctx.Info("Error writing archive cache: " + err.Error()) + } + return nil +} + func link(ctx *types.Context, objectFiles paths.PathList, coreDotARelPath *paths.Path, coreArchiveFilePath *paths.Path, buildProperties *properties.Map) error { objectFileList := strings.Join(utils.Map(objectFiles.AsStrings(), wrapWithDoubleQuotes), " ") // If command line length is too big (> 30000 chars), try to collect the object files into archives // and use that archives to complete the build. if len(objectFileList) > 30000 { + buildObjectFiles := objectFiles.Clone() + buildObjectFiles.FilterOutSuffix(".a") + buildArchiveFiles := objectFiles.Clone() + buildArchiveFiles.FilterSuffix(".a") // We must create an object file for each visited directory: this is required becuase gcc-ar checks // if an object file is already in the archive by looking ONLY at the filename WITHOUT the path, so // it may happen that a subdir/spi.o inside the archive may be overwritten by a anotherdir/spi.o // because thery are both named spi.o. - properties := buildProperties.Clone() - archives := paths.NewPathList() - for _, object := range objectFiles { - if object.HasSuffix(".a") { - archives.Add(object) - continue - } - archive := object.Parent().Join("objs.a") - if !archives.Contains(archive) { - archives.Add(archive) - // Cleanup old archives - _ = archive.Remove() + // Split objects by directory and create a CppArchive for each directory + archives := []*CppArchive{} + { + generatedArchivesFiles := map[string]*CppArchive{} + for _, object := range buildObjectFiles { + archive := object.Parent().Join("objs.a") + a := generatedArchivesFiles[archive.String()] + if a == nil { + a = NewCppArchive(archive) + archives = append(archives, a) + generatedArchivesFiles[archive.String()] = a + } + a.AddObject(object) } - properties.Set("archive_file", archive.Base()) - properties.SetPath("archive_file_path", archive) - properties.SetPath("object_file", object) + } - command, err := builder_utils.PrepareCommandForRecipe(properties, constants.RECIPE_AR_PATTERN, false, ctx.PackageManager.GetEnvVarsForSpawnedProcess()) - if err != nil { - return errors.WithStack(err) - } + arPattern := buildProperties.ExpandPropsInString(buildProperties.Get("recipe.ar.pattern")) - if _, _, err := utils.ExecCommand(ctx, command, utils.ShowIfVerbose /* stdout */, utils.Show /* stderr */); err != nil { - return errors.WithStack(err) + filesToLink := paths.NewPathList() + for _, a := range archives { + filesToLink.Add(a.ArchivePath) + if a.IsUpToDate() { + if ctx.Verbose { + ctx.Info(fmt.Sprintf("%s %s", tr("Using previously build archive:"), a.ArchivePath)) + } + continue + } + if err := a.Create(ctx, arPattern); err != nil { + return err } } - objectFileList = strings.Join(utils.Map(archives.AsStrings(), wrapWithDoubleQuotes), " ") + // Add all remaining archives from the build + filesToLink.AddAll(buildArchiveFiles) + + objectFileList = strings.Join(utils.Map(filesToLink.AsStrings(), wrapWithDoubleQuotes), " ") objectFileList = "-Wl,--whole-archive " + objectFileList + " -Wl,--no-whole-archive" }