Skip to content

Commit b53fd99

Browse files
cmaglieper1234
andauthored
Added gRPC functions to manage libraries in profiles (#3019)
* Added gRPC functions to manage libraries in profiles * Removed ProfileDump gRPC command. As stated by @per1234: > Build profile data is already provided via the LoadSketch method, so it > seems that even a mechanism that is truly for getting profile data should > be implemented by simply expanding the SketchProfile message to contain > all the data of the build profile (actually kind of silly that it > currently only provides a subset of the profile data). #3019 (comment) * Moved SketchProfileLibraryReference in the proper commono.proto file * linter: removed unneeded type specification * Renamed SketchProfileLibraryReference -> ProfileLibraryReference * Renamed InitProfile -> ProfileCreate And also corresponding messages: InitProfileRequest -> ProfileCreateRequest InitProfileResponse -> ProfileCreateResponse * Small refactoring, no code change * Fixed error messages * Removed unnecessary type specifier * Refactored libraryResolveDependencies. The function is now split into two functions: - librariesGetAllInstalled that requires a librariesmanager.Explorer. - libraryResolveDependencies that requires only a librariesindex.Index and do not require anymore a librariesmanager.Explorer. * Added support for 'dependency:' field in profiles libraries * ProfileLibAdd and ProfileLibRemove can now cleanup unneeded dependencies * Better error messages * Simplified Profile.RemoveLibrary(...) method. * Fixed algorithm for determination of required deps * Updated docs * Rename DuplicateProfileError -> ProfileAlreadyExitsError * Removed useless field in gRPC ProfileCreateResponse * fix: ProfileCreate sets the new profile as default only if asked to do so * Improved docs Co-authored-by: Per Tillisch <[email protected]> * Applied code review suggestion * Using cmp.Or helper --------- Co-authored-by: Per Tillisch <[email protected]>
1 parent 8f81c72 commit b53fd99

22 files changed

+3470
-704
lines changed

commands/cmderrors/cmderrors.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func composeErrorMsg(msg string, cause error) string {
3131
if cause == nil {
3232
return msg
3333
}
34+
if msg == "" {
35+
return cause.Error()
36+
}
3437
return fmt.Sprintf("%v: %v", msg, cause)
3538
}
3639

@@ -212,6 +215,20 @@ func (e *UnknownProfileError) GRPCStatus() *status.Status {
212215
return status.New(codes.NotFound, e.Error())
213216
}
214217

218+
// ProfileAlreadyExitsError is returned when the profile is a duplicate of an already existing one
219+
type ProfileAlreadyExitsError struct {
220+
Profile string
221+
}
222+
223+
func (e *ProfileAlreadyExitsError) Error() string {
224+
return i18n.Tr("Profile '%s' already exists", e.Profile)
225+
}
226+
227+
// GRPCStatus converts the error into a *status.Status
228+
func (e *ProfileAlreadyExitsError) GRPCStatus() *status.Status {
229+
return status.New(codes.AlreadyExists, e.Error())
230+
}
231+
215232
// InvalidProfileError is returned when the profile has errors
216233
type InvalidProfileError struct {
217234
Cause error
@@ -456,7 +473,7 @@ func (e *PlatformLoadingError) Unwrap() error {
456473
return e.Cause
457474
}
458475

459-
// LibraryNotFoundError is returned when a platform is not found
476+
// LibraryNotFoundError is returned when a library is not found
460477
type LibraryNotFoundError struct {
461478
Library string
462479
Cause error
@@ -904,3 +921,15 @@ func (e *InstanceNeedsReinitialization) GRPCStatus() *status.Status {
904921
WithDetails(&rpc.InstanceNeedsReinitializationError{})
905922
return st
906923
}
924+
925+
// MissingProfileError is returned when the Profile is mandatory and not specified
926+
type MissingProfileError struct{}
927+
928+
func (e *MissingProfileError) Error() string {
929+
return i18n.Tr("Missing Profile name")
930+
}
931+
932+
// GRPCStatus converts the error into a *status.Status
933+
func (e *MissingProfileError) GRPCStatus() *status.Status {
934+
return status.New(codes.InvalidArgument, e.Error())
935+
}

commands/service_library_install.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,31 +67,40 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s
6767
return err
6868
}
6969

70-
toInstall := map[string]*rpc.LibraryDependencyStatus{}
70+
toInstall := map[string]*librariesindex.Release{}
7171
if req.GetNoDeps() {
72-
toInstall[req.GetName()] = &rpc.LibraryDependencyStatus{
73-
Name: req.GetName(),
74-
VersionRequired: req.GetVersion(),
72+
version, err := parseVersion(req.GetVersion())
73+
if err != nil {
74+
return err
7575
}
76+
libRelease, err := li.FindRelease(req.GetName(), version)
77+
if err != nil {
78+
return err
79+
}
80+
toInstall[libRelease.GetName()] = libRelease
7681
} else {
7782
// Obtain the library explorer from the instance
7883
lme, releaseLme, err := instances.GetLibraryManagerExplorer(req.GetInstance())
7984
if err != nil {
8085
return err
8186
}
8287

83-
res, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetNoOverwrite())
88+
var overrides []*librariesindex.Release
89+
if req.GetNoOverwrite() {
90+
overrides = librariesGetAllInstalled(lme, li)
91+
}
92+
deps, err := libraryResolveDependencies(li, req.GetName(), req.GetVersion(), overrides)
8493
releaseLme()
8594
if err != nil {
8695
return err
8796
}
8897

89-
for _, dep := range res.GetDependencies() {
98+
for _, dep := range deps {
9099
if existingDep, has := toInstall[dep.GetName()]; has {
91-
if existingDep.GetVersionRequired() != dep.GetVersionRequired() {
100+
if !existingDep.GetVersion().Equal(dep.GetVersion()) {
92101
err := errors.New(
93102
i18n.Tr("two different versions of the library %[1]s are required: %[2]s and %[3]s",
94-
dep.GetName(), dep.GetVersionRequired(), existingDep.GetVersionRequired()))
103+
dep.GetName(), dep.GetVersion(), existingDep.GetVersion()))
95104
return &cmderrors.LibraryDependenciesResolutionFailedError{Cause: err}
96105
}
97106
}
@@ -118,16 +127,7 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s
118127
// Find the libReleasesToInstall to install
119128
libReleasesToInstall := map[*librariesindex.Release]*librariesmanager.LibraryInstallPlan{}
120129
installLocation := libraries.FromRPCLibraryInstallLocation(req.GetInstallLocation())
121-
for _, lib := range toInstall {
122-
version, err := parseVersion(lib.GetVersionRequired())
123-
if err != nil {
124-
return err
125-
}
126-
libRelease, err := li.FindRelease(lib.GetName(), version)
127-
if err != nil {
128-
return err
129-
}
130-
130+
for _, libRelease := range toInstall {
131131
installTask, err := lmi.InstallPrerequisiteCheck(libRelease.Library.Name, libRelease.Version, installLocation)
132132
if err != nil {
133133
return err

commands/service_library_resolve_deps.go

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,11 @@ func (s *arduinoCoreServerImpl) LibraryResolveDependencies(ctx context.Context,
4343
return nil, err
4444
}
4545

46-
return libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetDoNotUpdateInstalledLibraries())
47-
}
48-
49-
func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesindex.Index,
50-
reqName, reqVersion string, noOverwrite bool) (*rpc.LibraryResolveDependenciesResponse, error) {
51-
version, err := parseVersion(reqVersion)
52-
if err != nil {
53-
return nil, err
46+
var overrides []*librariesindex.Release
47+
if req.GetDoNotUpdateInstalledLibraries() {
48+
overrides = librariesGetAllInstalled(lme, li)
5449
}
55-
56-
// Search the requested lib
57-
reqLibRelease, err := li.FindRelease(reqName, version)
50+
deps, err := libraryResolveDependencies(li, req.GetName(), req.GetVersion(), overrides)
5851
if err != nil {
5952
return nil, err
6053
}
@@ -65,33 +58,6 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
6558
installedLibs[lib.Library.Name] = lib.Library
6659
}
6760

68-
// Resolve all dependencies...
69-
var overrides []*librariesindex.Release
70-
if noOverwrite {
71-
libs := lme.FindAllInstalled()
72-
libs = libs.FilterByVersionAndInstallLocation(nil, libraries.User)
73-
for _, lib := range libs {
74-
if release, err := li.FindRelease(lib.Name, lib.Version); err == nil {
75-
overrides = append(overrides, release)
76-
}
77-
}
78-
}
79-
deps := li.ResolveDependencies(reqLibRelease, overrides)
80-
81-
// If no solution has been found
82-
if len(deps) == 0 {
83-
// Check if there is a problem with the first level deps
84-
for _, directDep := range reqLibRelease.GetDependencies() {
85-
if _, ok := li.Libraries[directDep.GetName()]; !ok {
86-
err := errors.New(i18n.Tr("dependency '%s' is not available", directDep.GetName()))
87-
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{Cause: err}
88-
}
89-
}
90-
91-
// Otherwise there is no possible solution, the depends field has an invalid formula
92-
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{}
93-
}
94-
9561
res := []*rpc.LibraryDependencyStatus{}
9662
for _, dep := range deps {
9763
// ...and add information on currently installed versions of the libraries
@@ -115,3 +81,47 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
11581
})
11682
return &rpc.LibraryResolveDependenciesResponse{Dependencies: res}, nil
11783
}
84+
85+
func librariesGetAllInstalled(lme *librariesmanager.Explorer, li *librariesindex.Index) []*librariesindex.Release {
86+
var overrides []*librariesindex.Release
87+
libs := lme.FindAllInstalled()
88+
libs = libs.FilterByVersionAndInstallLocation(nil, libraries.User)
89+
for _, lib := range libs {
90+
if release, err := li.FindRelease(lib.Name, lib.Version); err == nil {
91+
overrides = append(overrides, release)
92+
}
93+
}
94+
return overrides
95+
}
96+
97+
func libraryResolveDependencies(li *librariesindex.Index, reqName, reqVersion string, overrides []*librariesindex.Release) ([]*librariesindex.Release, error) {
98+
version, err := parseVersion(reqVersion)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
// Search the requested lib
104+
reqLibRelease, err := li.FindRelease(reqName, version)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
// Resolve all dependencies...
110+
deps := li.ResolveDependencies(reqLibRelease, overrides)
111+
112+
// If no solution has been found
113+
if len(deps) == 0 {
114+
// Check if there is a problem with the first level deps
115+
for _, directDep := range reqLibRelease.GetDependencies() {
116+
if _, ok := li.Libraries[directDep.GetName()]; !ok {
117+
err := errors.New(i18n.Tr("dependency '%s' is not available", directDep.GetName()))
118+
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{Cause: err}
119+
}
120+
}
121+
122+
// Otherwise there is no possible solution, the depends field has an invalid formula
123+
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{}
124+
}
125+
126+
return deps, nil
127+
}

commands/service_profile_init.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 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 commands
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
23+
"github.com/arduino/arduino-cli/commands/cmderrors"
24+
"github.com/arduino/arduino-cli/commands/internal/instances"
25+
"github.com/arduino/arduino-cli/internal/arduino/sketch"
26+
"github.com/arduino/arduino-cli/internal/i18n"
27+
"github.com/arduino/arduino-cli/pkg/fqbn"
28+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
29+
"github.com/arduino/go-paths-helper"
30+
)
31+
32+
// ProfileCreate creates a new project file if it does not exist. If a profile name with the associated FQBN is specified,
33+
// it is added to the project.
34+
func (s *arduinoCoreServerImpl) ProfileCreate(ctx context.Context, req *rpc.ProfileCreateRequest) (*rpc.ProfileCreateResponse, error) {
35+
// Returns an error if the main file is missing from the sketch so there is no need to check if the path exists
36+
sk, err := sketch.New(paths.New(req.GetSketchPath()))
37+
if err != nil {
38+
return nil, err
39+
}
40+
projectFilePath := sk.GetProjectPath()
41+
42+
if !projectFilePath.Exist() {
43+
err := projectFilePath.WriteFile([]byte("profiles: {}\n"))
44+
if err != nil {
45+
return nil, err
46+
}
47+
}
48+
49+
if req.GetProfileName() != "" {
50+
if req.GetFqbn() == "" {
51+
return nil, &cmderrors.MissingFQBNError{}
52+
}
53+
fqbn, err := fqbn.Parse(req.GetFqbn())
54+
if err != nil {
55+
return nil, &cmderrors.InvalidFQBNError{Cause: err}
56+
}
57+
58+
// Check that the profile name is unique
59+
if profile, _ := sk.GetProfile(req.ProfileName); profile != nil {
60+
return nil, &cmderrors.ProfileAlreadyExitsError{Profile: req.ProfileName}
61+
}
62+
63+
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
64+
if err != nil {
65+
return nil, err
66+
}
67+
defer release()
68+
if pme.Dirty() {
69+
return nil, &cmderrors.InstanceNeedsReinitialization{}
70+
}
71+
72+
// Automatically detect the target platform if it is installed on the user's machine
73+
_, targetPlatform, _, _, _, err := pme.ResolveFQBN(fqbn)
74+
if err != nil {
75+
if targetPlatform == nil {
76+
return nil, &cmderrors.PlatformNotFoundError{
77+
Platform: fmt.Sprintf("%s:%s", fqbn.Vendor, fqbn.Architecture),
78+
Cause: errors.New(i18n.Tr("platform not installed")),
79+
}
80+
}
81+
return nil, &cmderrors.InvalidFQBNError{Cause: err}
82+
}
83+
84+
newProfile := &sketch.Profile{Name: req.GetProfileName(), FQBN: req.GetFqbn()}
85+
// TODO: what to do with the PlatformIndexURL?
86+
newProfile.Platforms = append(newProfile.Platforms, &sketch.ProfilePlatformReference{
87+
Packager: targetPlatform.Platform.Package.Name,
88+
Architecture: targetPlatform.Platform.Architecture,
89+
Version: targetPlatform.Version,
90+
})
91+
92+
sk.Project.Profiles = append(sk.Project.Profiles, newProfile)
93+
if req.DefaultProfile {
94+
sk.Project.DefaultProfile = newProfile.Name
95+
}
96+
97+
err = projectFilePath.WriteFile([]byte(sk.Project.AsYaml()))
98+
if err != nil {
99+
return nil, err
100+
}
101+
}
102+
103+
return &rpc.ProfileCreateResponse{}, nil
104+
}

0 commit comments

Comments
 (0)