Skip to content

Commit 3dca438

Browse files
authored
Allow locally installed libraries in sketch profiles. (#2930)
* Allow directories in profile libraries * Updated docs * Dump custom libraries in profiles with --dump-profile command * Added integration tests * Fix integration test in Windows * Improved integration tests * Fixed flaky intergration test.
1 parent cf62bee commit 3dca438

File tree

10 files changed

+192
-20
lines changed

10 files changed

+192
-20
lines changed

commands/instances.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,24 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor
363363
} else {
364364
// Load libraries required for profile
365365
for _, libraryRef := range profile.Libraries {
366+
if libraryRef.InstallDir != nil {
367+
libDir := libraryRef.InstallDir
368+
if !libDir.IsAbs() {
369+
libDir = paths.New(req.GetSketchPath()).JoinPath(libraryRef.InstallDir)
370+
}
371+
if !libDir.IsDir() {
372+
return &cmderrors.InvalidArgumentError{
373+
Message: i18n.Tr("Invalid library directory in sketch project: %s", libraryRef.InstallDir),
374+
}
375+
}
376+
lmb.AddLibrariesDir(librariesmanager.LibrariesDir{
377+
Path: libDir,
378+
Location: libraries.Unmanaged,
379+
IsSingleLibrary: true,
380+
})
381+
continue
382+
}
383+
366384
uid := libraryRef.InternalUniqueIdentifier()
367385
libRoot := s.settings.ProfilesCacheDir().Join(uid)
368386
libDir := libRoot.Join(libraryRef.Library)

docs/sketch-project-file.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Each profile will define:
1414
- The target core platform name and version (with the 3rd party platform index URL if needed)
1515
- A possible core platform name and version, that is a dependency of the target core platform (with the 3rd party
1616
platform index URL if needed)
17-
- The libraries used in the sketch (including their version)
17+
- A list of libraries used in the sketch. Each library could be:
18+
- a library taken from the Arduino Libraries Index
19+
- a library installed anywhere in the filesystem
1820
- The port and protocol to upload the sketch and monitor the board
1921

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

internal/arduino/sketch/profiles.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/arduino/arduino-cli/internal/i18n"
2929
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
3030
"github.com/arduino/go-paths-helper"
31+
"go.bug.st/f"
3132
semver "go.bug.st/relaxed-semver"
3233
"gopkg.in/yaml.v3"
3334
)
@@ -271,12 +272,26 @@ func (p *ProfilePlatformReference) UnmarshalYAML(unmarshal func(interface{}) err
271272

272273
// ProfileLibraryReference is a reference to a library
273274
type ProfileLibraryReference struct {
274-
Library string
275-
Version *semver.Version
275+
Library string
276+
InstallDir *paths.Path
277+
Version *semver.Version
276278
}
277279

278280
// UnmarshalYAML decodes a ProfileLibraryReference from YAML source.
279281
func (l *ProfileLibraryReference) UnmarshalYAML(unmarshal func(interface{}) error) error {
282+
var dataMap map[string]any
283+
if err := unmarshal(&dataMap); err == nil {
284+
if installDir, ok := dataMap["dir"]; !ok {
285+
return errors.New(i18n.Tr("invalid library reference: %s", dataMap))
286+
} else if installDir, ok := installDir.(string); !ok {
287+
return fmt.Errorf("%s: %s", i18n.Tr("invalid library reference: %s"), dataMap)
288+
} else {
289+
l.InstallDir = paths.New(installDir)
290+
l.Library = l.InstallDir.Base()
291+
return nil
292+
}
293+
}
294+
280295
var data string
281296
if err := unmarshal(&data); err != nil {
282297
return err
@@ -294,16 +309,23 @@ func (l *ProfileLibraryReference) UnmarshalYAML(unmarshal func(interface{}) erro
294309

295310
// AsYaml outputs the required library as Yaml
296311
func (l *ProfileLibraryReference) AsYaml() string {
297-
res := fmt.Sprintf(" - %s (%s)\n", l.Library, l.Version)
298-
return res
312+
if l.InstallDir != nil {
313+
return fmt.Sprintf(" - dir: %s\n", l.InstallDir)
314+
}
315+
return fmt.Sprintf(" - %s (%s)\n", l.Library, l.Version)
299316
}
300317

301318
func (l *ProfileLibraryReference) String() string {
319+
if l.InstallDir != nil {
320+
return fmt.Sprintf("%s@dir:%s", l.Library, l.InstallDir)
321+
}
302322
return fmt.Sprintf("%s@%s", l.Library, l.Version)
303323
}
304324

305325
// InternalUniqueIdentifier returns the unique identifier for this object
306326
func (l *ProfileLibraryReference) InternalUniqueIdentifier() string {
327+
f.Assert(l.InstallDir == nil,
328+
"InternalUniqueIdentifier should not be called for library references with an install directory")
307329
id := l.String()
308330
h := sha256.Sum256([]byte(id))
309331
res := fmt.Sprintf("%s_%s", id, hex.EncodeToString(h[:])[:16])

internal/cli/compile/compile.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"io"
2323
"os"
24+
"path/filepath"
2425
"strings"
2526

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

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

344346
newProfileName := "my_profile_name"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2022-2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package sketch_test
17+
18+
import (
19+
"encoding/json"
20+
"strings"
21+
"testing"
22+
23+
"github.com/arduino/arduino-cli/internal/integrationtest"
24+
"github.com/arduino/go-paths-helper"
25+
"github.com/stretchr/testify/require"
26+
"go.bug.st/testifyjson/requirejson"
27+
)
28+
29+
func TestSketchProfileDump(t *testing.T) {
30+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
31+
t.Cleanup(env.CleanUp)
32+
33+
// Prepare the sketch with libraries
34+
tmpDir, err := paths.MkTempDir("", "")
35+
require.NoError(t, err)
36+
t.Cleanup(func() { _ = tmpDir.RemoveAll })
37+
38+
sketchTemplate, err := paths.New("testdata", "SketchWithLibrary").Abs()
39+
require.NoError(t, err)
40+
41+
sketch := tmpDir.Join("SketchWithLibrary")
42+
libInside := sketch.Join("libraries", "MyLib")
43+
err = sketchTemplate.CopyDirTo(sketch)
44+
require.NoError(t, err)
45+
46+
libOutsideTemplate := sketchTemplate.Join("..", "MyLibOutside")
47+
libOutside := sketch.Join("..", "MyLibOutside")
48+
err = libOutsideTemplate.CopyDirTo(libOutside)
49+
require.NoError(t, err)
50+
51+
// Install the required core and libraries
52+
_, _, err = cli.Run("core", "install", "arduino:[email protected]")
53+
require.NoError(t, err)
54+
_, _, err = cli.Run("lib", "install", "Adafruit [email protected]", "--no-overwrite")
55+
require.NoError(t, err)
56+
_, _, err = cli.Run("lib", "install", "Adafruit GFX [email protected]", "--no-overwrite")
57+
require.NoError(t, err)
58+
_, _, err = cli.Run("lib", "install", "Adafruit [email protected]", "--no-overwrite")
59+
require.NoError(t, err)
60+
61+
// Check if the profile dump:
62+
// - keeps libraries in the sketch with a relative path
63+
// - keeps libraries outside the sketch with an absolute path
64+
// - keeps libraries installed in the system with just the name and version
65+
out, _, err := cli.Run("compile", "-b", "arduino:avr:uno",
66+
"--library", libInside.String(),
67+
"--library", libOutside.String(),
68+
"--dump-profile",
69+
sketch.String())
70+
require.NoError(t, err)
71+
require.Equal(t, strings.TrimSpace(`
72+
profiles:
73+
uno:
74+
fqbn: arduino:avr:uno
75+
platforms:
76+
- platform: arduino:avr (1.8.6)
77+
libraries:
78+
- dir: libraries/MyLib
79+
- dir: `+libOutside.String()+`
80+
- Adafruit SSD1306 (2.5.14)
81+
- Adafruit GFX Library (1.12.1)
82+
- Adafruit BusIO (1.17.1)
83+
`), strings.TrimSpace(string(out)))
84+
85+
// Dump the profile in the sketch directory and compile with it again
86+
err = sketch.Join("sketch.yaml").WriteFile(out)
87+
require.NoError(t, err)
88+
out, _, err = cli.Run("compile", "-m", "uno", "--json", sketch.String())
89+
require.NoError(t, err)
90+
// Check if local libraries are picked up correctly
91+
libInsideJson, _ := json.Marshal(libInside.String())
92+
libOutsideJson, _ := json.Marshal(libOutside.String())
93+
j := requirejson.Parse(t, out).Query(".builder_result.used_libraries")
94+
j.MustContain(`
95+
[
96+
{"name": "MyLib", "install_dir": ` + string(libInsideJson) + `},
97+
{"name": "MyLibOutside", "install_dir": ` + string(libOutsideJson) + `}
98+
]`)
99+
}

internal/integrationtest/sketch/testdata/MyLibOutside/MyLibOutside.h

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name=MyLibOutside
2+
version=1.3.7
3+
author=Arduino
4+
maintainer=Arduino <[email protected]>
5+
sentence=
6+
paragraph=
7+
category=Communication
8+
url=
9+
architectures=*
10+
includes=MyLibOutside.h
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <MyLib.h>
2+
#include <MyLibOutside.h>
3+
#include <Adafruit_SSD1306.h>
4+
5+
void setup() {}
6+
void loop() {}

internal/integrationtest/sketch/testdata/SketchWithLibrary/libraries/MyLib/MyLib.h

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name=MyLib
2+
version=1.3.7
3+
author=Arduino
4+
maintainer=Arduino <[email protected]>
5+
sentence=
6+
paragraph=
7+
category=Communication
8+
url=
9+
architectures=*
10+
includes=MyLib.h

0 commit comments

Comments
 (0)