Skip to content

Allow locally installed libraries in sketch profiles. #2930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions commands/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,24 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor
} else {
// Load libraries required for profile
for _, libraryRef := range profile.Libraries {
if libraryRef.InstallDir != nil {
libDir := libraryRef.InstallDir
if !libDir.IsAbs() {
libDir = paths.New(req.GetSketchPath()).JoinPath(libraryRef.InstallDir)
}
if !libDir.IsDir() {
return &cmderrors.InvalidArgumentError{
Message: i18n.Tr("Invalid library directory in sketch project: %s", libraryRef.InstallDir),
}
}
lmb.AddLibrariesDir(librariesmanager.LibrariesDir{
Path: libDir,
Location: libraries.Unmanaged,
IsSingleLibrary: true,
})
continue
}

uid := libraryRef.InternalUniqueIdentifier()
libRoot := s.settings.ProfilesCacheDir().Join(uid)
libDir := libRoot.Join(libraryRef.Library)
Expand Down
15 changes: 10 additions & 5 deletions docs/sketch-project-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Each profile will define:
- The target core platform name and version (with the 3rd party platform index URL if needed)
- A possible core platform name and version, that is a dependency of the target core platform (with the 3rd party
platform index URL if needed)
- The libraries used in the sketch (including their version)
- A list of libraries used in the sketch. Each library could be:
- a library taken from the Arduino Libraries Index
- a library installed anywhere in the filesystem
- The port and protocol to upload the sketch and monitor the board

The format of the file is the following:
Expand All @@ -31,9 +33,8 @@ profiles:
- platform: <PLATFORM_DEPENDENCY> (<PLATFORM_DEPENDENCY_VERSION>)
platform_index_url: <3RD_PARTY_PLATFORM_DEPENDENCY_URL>
libraries:
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)
- <INDEX_LIB_NAME> (<INDEX_LIB_VERSION>)
- dir: <LOCAL_LIB_PATH>
port: <PORT_NAME>
port_config:
<PORT_SETTING_NAME>: <PORT_SETTING_VALUE>
Expand All @@ -55,7 +56,11 @@ otherwise below). The available fields are:
information as `<PLATFORM>`, `<PLATFORM_VERSION>`, and `<3RD_PARTY_PLATFORM_URL>` respectively but for the core
platform dependency of the main core platform. These fields are optional.
- `libraries:` is a section where the required libraries to build the project are defined. This section is optional.
- `<LIB_VERSION>` is the version required for the library, for example, `1.0.0`.
- `<INDEX_LIB_NAME> (<INDEX_LIB_VERSION>)` represents a library from the Arduino Libraries Index, for example,
`MyLib (1.0.0)`.
- `dir: <LOCAL_LIB_PATH>` represents a library installed in the filesystem and `<LOCAL_LIB_PATH>` is the path to the
library. The path could be absolute or relative to the sketch folder. This option is available since Arduino CLI
1.3.0.
- `<USER_NOTES>` is a free text string available to the developer to add comments. This field is optional.
- `<PROGRAMMER>` is the programmer that will be used. This field is optional.

Expand Down
30 changes: 26 additions & 4 deletions internal/arduino/sketch/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/arduino/arduino-cli/internal/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"go.bug.st/f"
semver "go.bug.st/relaxed-semver"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -268,12 +269,26 @@ func (p *ProfilePlatformReference) UnmarshalYAML(unmarshal func(interface{}) err

// ProfileLibraryReference is a reference to a library
type ProfileLibraryReference struct {
Library string
Version *semver.Version
Library string
InstallDir *paths.Path
Version *semver.Version
}

// UnmarshalYAML decodes a ProfileLibraryReference from YAML source.
func (l *ProfileLibraryReference) UnmarshalYAML(unmarshal func(interface{}) error) error {
var dataMap map[string]any
if err := unmarshal(&dataMap); err == nil {
if installDir, ok := dataMap["dir"]; !ok {
return errors.New(i18n.Tr("invalid library reference: %s", dataMap))
} else if installDir, ok := installDir.(string); !ok {
return fmt.Errorf("%s: %s", i18n.Tr("invalid library reference: %s"), dataMap)
} else {
l.InstallDir = paths.New(installDir)
l.Library = l.InstallDir.Base()
return nil
}
}

var data string
if err := unmarshal(&data); err != nil {
return err
Expand All @@ -291,16 +306,23 @@ func (l *ProfileLibraryReference) UnmarshalYAML(unmarshal func(interface{}) erro

// AsYaml outputs the required library as Yaml
func (l *ProfileLibraryReference) AsYaml() string {
res := fmt.Sprintf(" - %s (%s)\n", l.Library, l.Version)
return res
if l.InstallDir != nil {
return fmt.Sprintf(" - dir: %s\n", l.InstallDir)
}
return fmt.Sprintf(" - %s (%s)\n", l.Library, l.Version)
}

func (l *ProfileLibraryReference) String() string {
if l.InstallDir != nil {
return fmt.Sprintf("%s@dir:%s", l.Library, l.InstallDir)
}
return fmt.Sprintf("%s@%s", l.Library, l.Version)
}

// InternalUniqueIdentifier returns the unique identifier for this object
func (l *ProfileLibraryReference) InternalUniqueIdentifier() string {
f.Assert(l.InstallDir == nil,
"InternalUniqueIdentifier should not be called for library references with an install directory")
id := l.String()
h := sha256.Sum256([]byte(id))
res := fmt.Sprintf("%s_%s", id, hex.EncodeToString(h[:])[:16])
Expand Down
24 changes: 13 additions & 11 deletions internal/cli/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/arduino/arduino-cli/commands"
Expand Down Expand Up @@ -323,22 +324,23 @@ func runCompileCommand(cmd *cobra.Command, args []string, srv rpc.ArduinoCoreSer
// Output profile

libs := ""
hasVendoredLibs := false
for _, lib := range builderRes.GetUsedLibraries() {
if lib.GetLocation() != rpc.LibraryLocation_LIBRARY_LOCATION_USER && lib.GetLocation() != rpc.LibraryLocation_LIBRARY_LOCATION_UNMANAGED {
continue
}
if lib.GetVersion() == "" {
hasVendoredLibs = true
continue
if lib.GetVersion() == "" || lib.Location == rpc.LibraryLocation_LIBRARY_LOCATION_UNMANAGED {
libDir := paths.New(lib.GetInstallDir())
// If the library is installed in the sketch path, we want to output the relative path
// to the sketch path, so that the sketch is portable.
if ok, err := libDir.IsInsideDir(sketchPath); err == nil && ok {
if ref, err := libDir.RelFrom(sketchPath); err == nil {
libDir = paths.New(filepath.ToSlash(ref.String()))
}
}
libs += fmt.Sprintln(" - dir: " + libDir.String())
} else {
libs += fmt.Sprintln(" - " + lib.GetName() + " (" + lib.GetVersion() + ")")
}
libs += fmt.Sprintln(" - " + lib.GetName() + " (" + lib.GetVersion() + ")")
}
if hasVendoredLibs {
msg := "\n"
msg += i18n.Tr("WARNING: The sketch is compiled using one or more custom libraries.") + "\n"
msg += i18n.Tr("Currently, Build Profiles only support libraries available through Arduino Library Manager.")
feedback.Warning(msg)
}

newProfileName := "my_profile_name"
Expand Down
99 changes: 99 additions & 0 deletions internal/integrationtest/sketch/profiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// This file is part of arduino-cli.
//
// Copyright 2022-2025 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package sketch_test

import (
"encoding/json"
"strings"
"testing"

"github.com/arduino/arduino-cli/internal/integrationtest"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
"go.bug.st/testifyjson/requirejson"
)

func TestSketchProfileDump(t *testing.T) {
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
t.Cleanup(env.CleanUp)

// Prepare the sketch with libraries
tmpDir, err := paths.MkTempDir("", "")
require.NoError(t, err)
t.Cleanup(func() { _ = tmpDir.RemoveAll })

sketchTemplate, err := paths.New("testdata", "SketchWithLibrary").Abs()
require.NoError(t, err)

sketch := tmpDir.Join("SketchWithLibrary")
libInside := sketch.Join("libraries", "MyLib")
err = sketchTemplate.CopyDirTo(sketch)
require.NoError(t, err)

libOutsideTemplate := sketchTemplate.Join("..", "MyLibOutside")
libOutside := sketch.Join("..", "MyLibOutside")
err = libOutsideTemplate.CopyDirTo(libOutside)
require.NoError(t, err)

// Install the required core and libraries
_, _, err = cli.Run("core", "install", "arduino:avr@1.8.6")
require.NoError(t, err)
_, _, err = cli.Run("lib", "install", "Adafruit BusIO@1.17.1")
require.NoError(t, err)
_, _, err = cli.Run("lib", "install", "Adafruit GFX Library@1.12.1")
require.NoError(t, err)
_, _, err = cli.Run("lib", "install", "Adafruit SSD1306@2.5.14")
require.NoError(t, err)

// Check if the profile dump:
// - keeps libraries in the sketch with a relative path
// - keeps libraries outside the sketch with an absolute path
// - keeps libraries installed in the system with just the name and version
out, _, err := cli.Run("compile", "-b", "arduino:avr:uno",
"--library", libInside.String(),
"--library", libOutside.String(),
"--dump-profile",
sketch.String())
require.NoError(t, err)
require.Equal(t, strings.TrimSpace(`
profiles:
uno:
fqbn: arduino:avr:uno
platforms:
- platform: arduino:avr (1.8.6)
libraries:
- dir: libraries/MyLib
- dir: `+libOutside.String()+`
- Adafruit SSD1306 (2.5.14)
- Adafruit GFX Library (1.12.1)
- Adafruit BusIO (1.17.1)
`), strings.TrimSpace(string(out)))

// Dump the profile in the sketch directory and compile with it again
err = sketch.Join("sketch.yaml").WriteFile(out)
require.NoError(t, err)
out, _, err = cli.Run("compile", "-m", "uno", "--json", sketch.String())
require.NoError(t, err)
// Check if local libraries are picked up correctly
libInsideJson, _ := json.Marshal(libInside.String())
libOutsideJson, _ := json.Marshal(libOutside.String())
j := requirejson.Parse(t, out).Query(".builder_result.used_libraries")
j.MustContain(`
[
{"name": "MyLib", "install_dir": ` + string(libInsideJson) + `},
{"name": "MyLibOutside", "install_dir": ` + string(libOutsideJson) + `}
]`)
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name=MyLibOutside
version=1.3.7
author=Arduino
maintainer=Arduino <info@arduino.cc>
sentence=
paragraph=
category=Communication
url=
architectures=*
includes=MyLibOutside.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include <MyLib.h>
#include <MyLibOutside.h>
#include <Adafruit_SSD1306.h>

void setup() {}
void loop() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name=MyLib
version=1.3.7
author=Arduino
maintainer=Arduino <info@arduino.cc>
sentence=
paragraph=
category=Communication
url=
architectures=*
includes=MyLib.h