Skip to content

Commit 47d7788

Browse files
committed
Added gRPC functions to manage libraries in profiles
1 parent 20e315c commit 47d7788

15 files changed

+2927
-636
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+
// DuplicateProfileError is returned when the profile is a duplicate of an already existing one
219+
type DuplicateProfileError struct {
220+
Profile string
221+
}
222+
223+
func (e *DuplicateProfileError) 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 *DuplicateProfileError) 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: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,31 +67,36 @@ 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
75+
}
76+
libRelease, err := li.FindRelease(req.GetName(), version)
77+
if err != nil {
78+
return err
7579
}
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+
deps, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetNoOverwrite())
8489
releaseLme()
8590
if err != nil {
8691
return err
8792
}
8893

89-
for _, dep := range res.GetDependencies() {
94+
for _, dep := range deps {
9095
if existingDep, has := toInstall[dep.GetName()]; has {
91-
if existingDep.GetVersionRequired() != dep.GetVersionRequired() {
96+
if !existingDep.GetVersion().Equal(dep.GetVersion()) {
9297
err := errors.New(
9398
i18n.Tr("two different versions of the library %[1]s are required: %[2]s and %[3]s",
94-
dep.GetName(), dep.GetVersionRequired(), existingDep.GetVersionRequired()))
99+
dep.GetName(), dep.GetVersion(), existingDep.GetVersion()))
95100
return &cmderrors.LibraryDependenciesResolutionFailedError{Cause: err}
96101
}
97102
}
@@ -118,16 +123,7 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s
118123
// Find the libReleasesToInstall to install
119124
libReleasesToInstall := map[*librariesindex.Release]*librariesmanager.LibraryInstallPlan{}
120125
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-
126+
for _, libRelease := range toInstall {
131127
installTask, err := lmi.InstallPrerequisiteCheck(libRelease.Library.Name, libRelease.Version, installLocation)
132128
if err != nil {
133129
return err

commands/service_library_resolve_deps.go

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

46-
return libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetDoNotUpdateInstalledLibraries())
46+
deps, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetDoNotUpdateInstalledLibraries())
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
// Extract all installed libraries
52+
installedLibs := map[string]*libraries.Library{}
53+
for _, lib := range listLibraries(lme, li, false, false) {
54+
installedLibs[lib.Library.Name] = lib.Library
55+
}
56+
57+
res := []*rpc.LibraryDependencyStatus{}
58+
for _, dep := range deps {
59+
// ...and add information on currently installed versions of the libraries
60+
var installed *semver.Version
61+
required := dep.GetVersion()
62+
if installedLib, has := installedLibs[dep.GetName()]; has {
63+
installed = installedLib.Version
64+
if installed != nil && required != nil && installed.Equal(required) {
65+
// avoid situations like installed=0.53 and required=0.53.0
66+
required = installed
67+
}
68+
}
69+
res = append(res, &rpc.LibraryDependencyStatus{
70+
Name: dep.GetName(),
71+
VersionRequired: required.String(),
72+
VersionInstalled: installed.String(),
73+
})
74+
}
75+
sort.Slice(res, func(i, j int) bool {
76+
return res[i].GetName() < res[j].GetName()
77+
})
78+
return &rpc.LibraryResolveDependenciesResponse{Dependencies: res}, nil
4779
}
4880

4981
func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesindex.Index,
50-
reqName, reqVersion string, noOverwrite bool) (*rpc.LibraryResolveDependenciesResponse, error) {
82+
reqName, reqVersion string, noOverwrite bool) ([]*librariesindex.Release, error) {
5183
version, err := parseVersion(reqVersion)
5284
if err != nil {
5385
return nil, err
@@ -59,12 +91,6 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
5991
return nil, err
6092
}
6193

62-
// Extract all installed libraries
63-
installedLibs := map[string]*libraries.Library{}
64-
for _, lib := range listLibraries(lme, li, false, false) {
65-
installedLibs[lib.Library.Name] = lib.Library
66-
}
67-
6894
// Resolve all dependencies...
6995
var overrides []*librariesindex.Release
7096
if noOverwrite {
@@ -92,26 +118,5 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
92118
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{}
93119
}
94120

95-
res := []*rpc.LibraryDependencyStatus{}
96-
for _, dep := range deps {
97-
// ...and add information on currently installed versions of the libraries
98-
var installed *semver.Version
99-
required := dep.GetVersion()
100-
if installedLib, has := installedLibs[dep.GetName()]; has {
101-
installed = installedLib.Version
102-
if installed != nil && required != nil && installed.Equal(required) {
103-
// avoid situations like installed=0.53 and required=0.53.0
104-
required = installed
105-
}
106-
}
107-
res = append(res, &rpc.LibraryDependencyStatus{
108-
Name: dep.GetName(),
109-
VersionRequired: required.String(),
110-
VersionInstalled: installed.String(),
111-
})
112-
}
113-
sort.Slice(res, func(i, j int) bool {
114-
return res[i].GetName() < res[j].GetName()
115-
})
116-
return &rpc.LibraryResolveDependenciesResponse{Dependencies: res}, nil
121+
return deps, nil
117122
}

commands/service_profile_dump.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
"encoding/json"
21+
"fmt"
22+
23+
"github.com/arduino/arduino-cli/commands/cmderrors"
24+
"github.com/arduino/arduino-cli/internal/arduino/sketch"
25+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
26+
"github.com/arduino/go-paths-helper"
27+
)
28+
29+
// ProfileDump dumps the content of the project file.
30+
func (s *arduinoCoreServerImpl) ProfileDump(ctx context.Context, req *rpc.ProfileDumpRequest) (*rpc.ProfileDumpResponse, error) {
31+
sk, err := sketch.New(paths.New(req.GetSketchPath()))
32+
if err != nil {
33+
return nil, err
34+
}
35+
switch req.GetDumpFormat() {
36+
case "yaml":
37+
return &rpc.ProfileDumpResponse{EncodedProfile: sk.Project.AsYaml()}, nil
38+
case "", "json":
39+
data, err := json.MarshalIndent(sk.Project, "", " ")
40+
if err != nil {
41+
return nil, fmt.Errorf("error marshalling settings: %v", err)
42+
}
43+
return &rpc.ProfileDumpResponse{EncodedProfile: string(data)}, nil
44+
default:
45+
return nil, &cmderrors.InvalidArgumentError{Message: fmt.Sprintf("unsupported format: %s", req.GetDumpFormat())}
46+
}
47+
}

commands/service_profile_init.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
// InitProfile 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) InitProfile(ctx context.Context, req *rpc.InitProfileRequest) (*rpc.InitProfileResponse, 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.DuplicateProfileError{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+
// Set the profile as the default one if it's the only one
94+
if req.DefaultProfile || len(sk.Project.Profiles) == 1 {
95+
sk.Project.DefaultProfile = newProfile.Name
96+
}
97+
98+
err = projectFilePath.WriteFile([]byte(sk.Project.AsYaml()))
99+
if err != nil {
100+
return nil, err
101+
}
102+
}
103+
104+
return &rpc.InitProfileResponse{ProjectFilePath: projectFilePath.String()}, nil
105+
}

0 commit comments

Comments
 (0)