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 license@arduino.cc.
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:avr@1.8.6")
53+
require.NoError(t, err)
54+
_, _, err = cli.Run("lib", "install", "Adafruit BusIO@1.17.1", "--no-overwrite")
55+
require.NoError(t, err)
56+
_, _, err = cli.Run("lib", "install", "Adafruit GFX Library@1.12.1", "--no-overwrite")
57+
require.NoError(t, err)
58+
_, _, err = cli.Run("lib", "install", "Adafruit SSD1306@2.5.14", "--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 <info@arduino.cc>
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 <info@arduino.cc>
5+
sentence=
6+
paragraph=
7+
category=Communication
8+
url=
9+
architectures=*
10+
includes=MyLib.h

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy