Commit ae576110 authored by Cristian Maglie's avatar Cristian Maglie

Added LibrariesResolver and LibrariesList

parent adbd8beb
......@@ -15,7 +15,7 @@
"types",
"utils"
]
revision = "4722d661a3ceb9adff5f9a26ea558d6c110ace6e"
revision = "22a1e71c1f968a6e6c184b3ae26699197678ed15"
source = "github.com/bcmi-labs/arduino-builder"
[[projects]]
......
......@@ -30,6 +30,8 @@
package libraries
import (
"fmt"
"github.com/arduino/go-paths-helper"
)
......@@ -80,22 +82,71 @@ func (library *Library) String() string {
return library.Name // + " : " + library.SrcFolder.String()
}
// SupportsAnyArchitectureIn check if the library supports at least one of
// the given architectures
func (library *Library) SupportsAnyArchitectureIn(archs []string) bool {
for _, libArch := range library.Architectures {
if libArch == "*" {
// SupportsAnyArchitectureIn returns true if any of the following is true:
// - the library supports at least one of the given architectures
// - the library is architecture independent
// - the library doesn't specify any `architecture` field in library.properties
func (library *Library) SupportsAnyArchitectureIn(archs ...string) bool {
if len(library.Architectures) == 0 {
return true
}
if library.IsArchitectureIndependent() {
return true
}
for _, arch := range archs {
if arch == "*" || library.IsOptimizedForArchitecture(arch) {
return true
}
for _, arch := range archs {
if arch == libArch || arch == "*" {
return true
}
}
return false
}
// IsOptimizedForArchitecture returns true if the library declares to be
// explicitly compatible for a specific architecture (the `architecture` field
// in library.properties contains the architecture passed as parameter)
func (library *Library) IsOptimizedForArchitecture(arch string) bool {
for _, libArch := range library.Architectures {
if libArch == arch {
return true
}
}
return false
}
// IsArchitectureIndependent returns true if the library declares to be
// compatibile with all architectures (the `architecture` field in
// library.properties contains the `*` item)
func (library *Library) IsArchitectureIndependent() bool {
return library.IsOptimizedForArchitecture("*")
}
// PriorityForArchitecture returns an integer that represents the
// priority this lib has for the specified architecture based on
// his location and the architectures directly supported (as exposed
// on the `architecture` field of the `library.properties`)
// This function returns an integer between 0 and 255, higher means
// higher priority.
func (library *Library) PriorityForArchitecture(arch string) uint8 {
bonus := uint8(0)
// Bonus for core-optimized libraries
if library.IsOptimizedForArchitecture(arch) {
bonus = 0x10
}
switch library.Location {
case IDEBuiltIn:
return bonus + 0x00
case ReferencedPlatformBuiltIn:
return bonus + 0x01
case PlatformBuiltIn:
return bonus + 0x02
case Sketchbook:
return bonus + 0x03
}
panic(fmt.Sprintf("Invalid library location: %d", library.Location))
}
// SourceDir represents a source dir of a library
type SourceDir struct {
Folder *paths.Path
......
/*
* This file is part of arduino-cli.
*
* arduino-cli is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* As a special exception, you may use this file as part of a free software
* library without restriction. Specifically, if other files instantiate
* templates or use macros or inline functions from this file, or you compile
* this file and link it with other files to produce an executable, this
* file does not by itself cause the resulting executable to be covered by
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
*
* Copyright 2017 ARDUINO AG (http://www.arduino.cc/)
*/
package libraries
import (
"sort"
)
// List is a list of Libraries
type List []*Library
// Contains check if a lib is contained in the list
func (list *List) Contains(lib *Library) bool {
for _, l := range *list {
if l == lib {
return true
}
}
return false
}
// Add appends all libraries passed as parameter in the list
func (list *List) Add(libs ...*Library) {
for _, lib := range libs {
*list = append(*list, lib)
}
}
// FindByName returns the first library in the list that match
// the specified name or nil if not found
func (list *List) FindByName(name string) *Library {
for _, lib := range *list {
if lib.Name == name {
return lib
}
}
return nil
}
// SortByArchitecturePriority sorts the libraries in descending order using
// the Arduino lib priority ordering (the first has the higher priority)
func (list *List) SortByArchitecturePriority(arch string) {
sort.Slice(*list, func(i, j int) bool {
a, b := (*list)[i], (*list)[j]
return a.PriorityForArchitecture(arch) > b.PriorityForArchitecture(arch)
})
}
/*
// HasHigherPriority returns true if library x has higher priority compared to library
// y for the given header and architecture.
func HasHigherPriority(libX, libY *Library, header string, arch string) bool {
//return computePriority(libX, header, arch) > computePriority(libY, header, arch)
header = strings.TrimSuffix(header, filepath.Ext(header))
simplify := func(name string) string {
name = utils.SanitizeName(name)
name = strings.ToLower(name)
return name
}
header = simplify(header)
nameX := simplify(libX.Name)
nameY := simplify(libY.Name)
compareLocations := func() bool {
// XXX: priority inversion case.
if libX.Location < libY.Location {
return true
}
return false
}
checks := []func(name, header string) bool{
func(name, header string) bool { return name == header },
func(name, header string) bool { return name == header+"-master" },
strings.HasPrefix,
strings.HasSuffix,
strings.Contains,
}
// Run all checks to sort priorities based on library name
// If both library match the same name check, then fallback to
// compare locations
for _, check := range checks {
x := check(nameX, header)
y := check(nameY, header)
if x && y {
return compareLocations()
}
if x {
return true
}
if y {
return false
}
}
return compareLocations()
}
*/
/*
* This file is part of arduino-cli.
*
* Copyright 2018 ARDUINO AG (http://www.arduino.cc/)
*
* arduino-cli is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* As a special exception, you may use this file as part of a free software
* library without restriction. Specifically, if other files instantiate
* templates or use macros or inline functions from this file, or you compile
* this file and link it with other files to produce an executable, this
* file does not by itself cause the resulting executable to be covered by
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
*/
package librariesresolver
import (
"fmt"
"path/filepath"
"strings"
"github.com/bcmi-labs/arduino-cli/arduino/libraries"
"github.com/bcmi-labs/arduino-cli/arduino/libraries/librariesmanager"
"github.com/bcmi-labs/arduino-cli/arduino/utils"
"github.com/sirupsen/logrus"
)
// Cpp finds libraries made for the C++ language
type Cpp struct {
headers map[string]libraries.List
}
// NewCppResolver creates a new Cpp resolver
func NewCppResolver() *Cpp {
return &Cpp{
headers: map[string]libraries.List{},
}
}
// ScanFromLibrariesManager reads all librariers loaded in the LibrariesManager to find
// and cache all C++ headers for later retrieval
func (resolver *Cpp) ScanFromLibrariesManager(lm *librariesmanager.LibrariesManager) error {
for _, libAlternatives := range lm.Libraries {
for _, lib := range libAlternatives.Alternatives {
resolver.ScanLibrary(lib)
}
}
return nil
}
// ScanLibrary reads a library to find and cache C++ headers for later retrieval
func (resolver *Cpp) ScanLibrary(lib *libraries.Library) error {
cppHeaders, err := lib.SrcFolder.ReadDir()
if err != nil {
return fmt.Errorf("reading lib src dir: %s", err)
}
cppHeaders.FilterSuffix(".h", ".hpp", ".hh")
for _, cppHeaderPath := range cppHeaders {
cppHeader := cppHeaderPath.Base()
l := resolver.headers[cppHeader]
l.Add(lib)
resolver.headers[cppHeader] = l
}
return nil
}
// AlternativesFor returns all the libraries that provides the specified header
func (resolver *Cpp) AlternativesFor(header string) libraries.List {
fmt.Printf("Alternatives for %s: %s\n", header, resolver.headers[header])
return resolver.headers[header]
}
// ResolveFor finds the most suitable library for the specified combination of
// header and architecture. If no libraries provides the requested header, nil is returned
func (resolver *Cpp) ResolveFor(header, architecture string) *libraries.Library {
logrus.Infof("Resolving include %s for arch %s", header, architecture)
var found *libraries.Library
var foundPriority int
for _, lib := range resolver.headers[header] {
libPriority := computePriority(lib, header, architecture)
msg := " discarded"
if found == nil || foundPriority < libPriority {
found = lib
foundPriority = libPriority
msg = " found better lib"
}
logrus.
WithField("lib", lib.Name).
WithField("prio", fmt.Sprintf("%03X", libPriority)).
Infof(msg)
}
return found
}
func computePriority(lib *libraries.Library, header, arch string) int {
simplify := func(name string) string {
name = utils.SanitizeName(name)
name = strings.ToLower(name)
return name
}
header = strings.TrimSuffix(header, filepath.Ext(header))
header = simplify(header)
name := simplify(lib.Name)
priority := int(lib.PriorityForArchitecture(arch)) // bewteen 0..255
if name == header {
priority += 0x500
} else if name == header+"-master" {
priority += 0x400
} else if strings.HasPrefix(name, header) {
priority += 0x300
} else if strings.HasSuffix(name, header) {
priority += 0x200
} else if strings.Contains(name, header) {
priority += 0x100
}
return priority
}
package librariesresolver
import (
"testing"
"github.com/bcmi-labs/arduino-cli/arduino/libraries"
"github.com/stretchr/testify/require"
)
func TestCppHeaderPriority(t *testing.T) {
l1 := &libraries.Library{Name: "Calculus Lib", Location: libraries.Sketchbook}
l2 := &libraries.Library{Name: "Calculus Lib-master", Location: libraries.Sketchbook}
l3 := &libraries.Library{Name: "Calculus Lib Improved", Location: libraries.Sketchbook}
l4 := &libraries.Library{Name: "Another Calculus Lib", Location: libraries.Sketchbook}
l5 := &libraries.Library{Name: "Yet Another Calculus Lib Improved", Location: libraries.Sketchbook}
l6 := &libraries.Library{Name: "AnotherLib", Location: libraries.Sketchbook}
r1 := computePriority(l1, "calculus_lib.h", "avr")
r2 := computePriority(l2, "calculus_lib.h", "avr")
r3 := computePriority(l3, "calculus_lib.h", "avr")
r4 := computePriority(l4, "calculus_lib.h", "avr")
r5 := computePriority(l5, "calculus_lib.h", "avr")
r6 := computePriority(l6, "calculus_lib.h", "avr")
require.True(t, r1 > r2)
require.True(t, r2 > r3)
require.True(t, r3 > r4)
require.True(t, r4 > r5)
require.True(t, r5 > r6)
}
......@@ -33,6 +33,7 @@ import (
"os"
"github.com/bcmi-labs/arduino-cli/arduino/libraries/librariesmanager"
"github.com/bcmi-labs/arduino-cli/arduino/libraries/librariesresolver"
"github.com/arduino/arduino-builder/i18n"
"github.com/arduino/arduino-builder/types"
......@@ -89,22 +90,11 @@ func (s *LibrariesLoader) Run(ctx *types.Context) error {
}
}
headerToLibraries := make(map[string][]*libraries.Library)
for _, lib := range lm.Libraries {
for _, library := range lib.Alternatives {
headers, err := library.SrcFolder.ReadDir()
if err != nil {
return i18n.WrapError(err)
}
headers.FilterSuffix(".h", ".hpp", ".hh")
for _, header := range headers {
headerFileName := header.Base()
headerToLibraries[headerFileName] = append(headerToLibraries[headerFileName], library)
}
}
resolver := librariesresolver.NewCppResolver()
if err := resolver.ScanFromLibrariesManager(lm); err != nil {
return i18n.WrapError(err)
}
ctx.HeaderToLibraries = headerToLibraries
ctx.LibrariesResolver = resolver
return nil
}
......@@ -71,7 +71,7 @@ func (s *LibrariesBuilder) Run(ctx *types.Context) error {
return nil
}
func fixLDFLAGforPrecompiledLibraries(ctx *types.Context, libs []*libraries.Library) error {
func fixLDFLAGforPrecompiledLibraries(ctx *types.Context, libs libraries.List) error {
for _, library := range libs {
if library.Precompiled {
......@@ -96,7 +96,7 @@ func fixLDFLAGforPrecompiledLibraries(ctx *types.Context, libs []*libraries.Libr
return nil
}
func compileLibraries(ctx *types.Context, libraries []*libraries.Library, buildPath *paths.Path, buildProperties properties.Map, includes []string) (paths.PathList, error) {
func compileLibraries(ctx *types.Context, libraries libraries.List, buildPath *paths.Path, buildProperties properties.Map, includes []string) (paths.PathList, error) {
objectFiles := paths.NewPathList()
for _, library := range libraries {
libraryObjectFiles, err := compileLibrary(ctx, library, buildPath, buildProperties, includes)
......
......@@ -30,98 +30,44 @@
package builder
import (
"path/filepath"
"strings"
"fmt"
"github.com/arduino/arduino-builder/constants"
"github.com/arduino/arduino-builder/types"
"github.com/arduino/arduino-builder/utils"
"github.com/bcmi-labs/arduino-cli/arduino/cores"
"github.com/bcmi-labs/arduino-cli/arduino/libraries"
)
func ResolveLibrary(ctx *types.Context, header string) *libraries.Library {
headerToLibraries := ctx.HeaderToLibraries
platforms := []*cores.PlatformRelease{ctx.ActualPlatform, ctx.TargetPlatform}
libraryResolutionResults := ctx.LibrariesResolutionResults
resolver := ctx.LibrariesResolver
importedLibraries := ctx.ImportedLibraries
libs := append([]*libraries.Library{}, headerToLibraries[header]...)
candidates := resolver.AlternativesFor(header)
fmt.Printf("ResolveLibrary(%s)\n", header)
fmt.Printf(" -> candidates: %s\n", candidates)
if libs == nil || len(libs) == 0 {
if candidates == nil || len(candidates) == 0 {
return nil
}
if importedLibraryContainsOneOfCandidates(importedLibraries, libs) {
return nil
}
if len(libs) == 1 {
return libs[0]
}
reverse(libs)
var library *libraries.Library
for _, platform := range platforms {
if platform != nil {
library = findBestLibraryWithHeader(header, librariesCompatibleWithPlatform(libs, platform, true))
for _, candidate := range candidates {
if importedLibraries.Contains(candidate) {
return nil
}
}
if library == nil {
library = findBestLibraryWithHeader(header, libs)
}
if library == nil {
// reorder libraries to promote fully compatible ones
for _, platform := range platforms {
if platform != nil {
libs = append(librariesCompatibleWithPlatform(libs, platform, false), libs...)
}
}
library = libs[0]
selected := resolver.ResolveFor(header, ctx.TargetPlatform.Platform.Architecture)
if alreadyImported := importedLibraries.FindByName(selected.Name); alreadyImported != nil {
selected = alreadyImported
}
library = useAlreadyImportedLibraryWithSameNameIfExists(library, importedLibraries)
libraryResolutionResults[header] = types.LibraryResolutionResult{
Library: library,
NotUsedLibraries: filterOutLibraryFrom(libs, library),
ctx.LibrariesResolutionResults[header] = types.LibraryResolutionResult{
Library: selected,
NotUsedLibraries: filterOutLibraryFrom(candidates, selected),
}
return library
}
//facepalm. sort.Reverse needs an Interface that implements Len/Less/Swap. It's a slice! What else for reversing it?!?
func reverse(data []*libraries.Library) {
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
data[i], data[j] = data[j], data[i]
}
return selected
}
func importedLibraryContainsOneOfCandidates(imported []*libraries.Library, candidates []*libraries.Library) bool {
for _, i := range imported {
for _, j := range candidates {
if i == j {
return true
}
}
}
return false
}
func useAlreadyImportedLibraryWithSameNameIfExists(library *libraries.Library, imported []*libraries.Library) *libraries.Library {
for _, lib := range imported {
if lib.Name == library.Name {
return lib
}
}
return library
}
func filterOutLibraryFrom(libs []*libraries.Library, libraryToRemove *libraries.Library) []*libraries.Library {
func filterOutLibraryFrom(libs libraries.List, libraryToRemove *libraries.Library) libraries.List {
filteredOutLibraries := []*libraries.Library{}
for _, lib := range libs {
if lib != libraryToRemove {
......@@ -130,107 +76,3 @@ func filterOutLibraryFrom(libs []*libraries.Library, libraryToRemove *libraries.
}
return filteredOutLibraries
}
func libraryCompatibleWithPlatform(library *libraries.Library, platform *cores.PlatformRelease) (bool, bool) {
if len(library.Architectures) == 0 {
return true, true
}
if utils.SliceContains(library.Architectures, constants.LIBRARY_ALL_ARCHS) {
return true, true
}
return utils.SliceContains(library.Architectures, platform.Platform.Architecture), false
}
func libraryCompatibleWithAllPlatforms(library *libraries.Library) bool {
if utils.SliceContains(library.Architectures, constants.LIBRARY_ALL_ARCHS) {
return true
}
return false
}
func librariesCompatibleWithPlatform(libs []*libraries.Library, platform *cores.PlatformRelease, reorder bool) []*libraries.Library {
var compatibleLibraries []*libraries.Library
for _, library := range libs {
compatible, generic := libraryCompatibleWithPlatform(library, platform)
if compatible {
if !generic && len(compatibleLibraries) != 0 && libraryCompatibleWithAllPlatforms(compatibleLibraries[0]) && reorder == true {
//priority inversion
compatibleLibraries = append([]*libraries.Library{library}, compatibleLibraries...)
} else {
compatibleLibraries = append(compatibleLibraries, library)
}
}
}
return compatibleLibraries
}
func findBestLibraryWithHeader(header string, libs []*libraries.Library) *libraries.Library {
headerName := strings.Replace(header, filepath.Ext(header), constants.EMPTY_STRING, -1)
var library *libraries.Library
for _, headerName := range []string{headerName, strings.ToLower(headerName)} {
library = findLibWithName(headerName, libs)
if library != nil {
return library
}
library = findLibWithName(headerName+"-master", libs)
if library != nil {
return library
}
library = findLibWithNameStartingWith(headerName, libs)
if library != nil {
return library
}
library = findLibWithNameEndingWith(headerName, libs)
if library != nil {
return library
}
library = findLibWithNameContaining(headerName, libs)
if library != nil {
return library
}
}
return nil
}
func findLibWithName(name string, libraries []*libraries.Library) *libraries.Library {
for _, library := range libraries {
if simplifyName(library.Name) == simplifyName(name) {
return library
}
}
return nil
}
func findLibWithNameStartingWith(name string, libraries []*libraries.Library) *libraries.Library {
for _, library := range libraries {
if strings.HasPrefix(simplifyName(library.Name), simplifyName(name)) {
return library
}
}
return nil
}
func findLibWithNameEndingWith(name string, libraries []*libraries.Library) *libraries.Library {
for _, library := range libraries {
if strings.HasSuffix(simplifyName(library.Name), simplifyName(name)) {
return library
}
}
return nil
}
func findLibWithNameContaining(name string, libraries []*libraries.Library) *libraries.Library {
for _, library := range libraries {
if strings.Contains(simplifyName(library.Name), simplifyName(name)) {
return library
}
}
return nil
}
func simplifyName(name string) string {
return strings.ToLower(strings.Replace(name, "_", " ", -1))
}
......@@ -4,6 +4,7 @@ import (
"strings"
"github.com/bcmi-labs/arduino-cli/arduino/libraries/librariesmanager"
"github.com/bcmi-labs/arduino-cli/arduino/libraries/librariesresolver"
"github.com/arduino/arduino-builder/i18n"
"github.com/arduino/go-paths-helper"
......@@ -75,8 +76,8 @@ type Context struct {
// Libraries handling
LibrariesManager *librariesmanager.LibrariesManager
HeaderToLibraries map[string][]*libraries.Library
ImportedLibraries []*libraries.Library
LibrariesResolver *librariesresolver.Cpp
ImportedLibraries libraries.List
LibrariesResolutionResults map[string]LibraryResolutionResult
IncludeFolders paths.PathList
//OutputGccMinusM string
......
......@@ -54,7 +54,7 @@ func (s *WarnAboutArchIncompatibleLibraries) Run(ctx *types.Context) error {
}
for _, importedLibrary := range ctx.ImportedLibraries {
if !importedLibrary.SupportsAnyArchitectureIn(archs) {
if !importedLibrary.SupportsAnyArchitectureIn(archs...) {
logger.Fprintln(os.Stdout, constants.LOG_LEVEL_WARN, constants.MSG_LIBRARY_INCOMPATIBLE_ARCH,
importedLibrary.Name,
strings.Join(importedLibrary.Architectures, ", "),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment