Skip to content
Draft
31 changes: 30 additions & 1 deletion commands/cmderrors/cmderrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func composeErrorMsg(msg string, cause error) string {
if cause == nil {
return msg
}
if msg == "" {
return cause.Error()
}
return fmt.Sprintf("%v: %v", msg, cause)
}

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

// DuplicateProfileError is returned when the profile is a duplicate of an already existing one
type DuplicateProfileError struct {
Profile string
}

func (e *DuplicateProfileError) Error() string {
return i18n.Tr("Profile '%s' already exists", e.Profile)
}

// GRPCStatus converts the error into a *status.Status
func (e *DuplicateProfileError) GRPCStatus() *status.Status {
return status.New(codes.AlreadyExists, e.Error())
}

// InvalidProfileError is returned when the profile has errors
type InvalidProfileError struct {
Cause error
Expand Down Expand Up @@ -456,7 +473,7 @@ func (e *PlatformLoadingError) Unwrap() error {
return e.Cause
}

// LibraryNotFoundError is returned when a platform is not found
// LibraryNotFoundError is returned when a library is not found
type LibraryNotFoundError struct {
Library string
Cause error
Expand Down Expand Up @@ -904,3 +921,15 @@ func (e *InstanceNeedsReinitialization) GRPCStatus() *status.Status {
WithDetails(&rpc.InstanceNeedsReinitializationError{})
return st
}

// MissingProfileError is returned when the Profile is mandatory and not specified
type MissingProfileError struct{}

func (e *MissingProfileError) Error() string {
return i18n.Tr("Missing Profile name")
}

// GRPCStatus converts the error into a *status.Status
func (e *MissingProfileError) GRPCStatus() *status.Status {
return status.New(codes.InvalidArgument, e.Error())
}
32 changes: 14 additions & 18 deletions commands/service_library_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,36 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s
return err
}

toInstall := map[string]*rpc.LibraryDependencyStatus{}
toInstall := map[string]*librariesindex.Release{}
if req.GetNoDeps() {
toInstall[req.GetName()] = &rpc.LibraryDependencyStatus{
Name: req.GetName(),
VersionRequired: req.GetVersion(),
version, err := parseVersion(req.GetVersion())
if err != nil {
return err
}
libRelease, err := li.FindRelease(req.GetName(), version)
if err != nil {
return err
}
toInstall[libRelease.GetName()] = libRelease
} else {
// Obtain the library explorer from the instance
lme, releaseLme, err := instances.GetLibraryManagerExplorer(req.GetInstance())
if err != nil {
return err
}

res, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetNoOverwrite())
deps, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetNoOverwrite())
releaseLme()
if err != nil {
return err
}

for _, dep := range res.GetDependencies() {
for _, dep := range deps {
if existingDep, has := toInstall[dep.GetName()]; has {
if existingDep.GetVersionRequired() != dep.GetVersionRequired() {
if !existingDep.GetVersion().Equal(dep.GetVersion()) {
err := errors.New(
i18n.Tr("two different versions of the library %[1]s are required: %[2]s and %[3]s",
dep.GetName(), dep.GetVersionRequired(), existingDep.GetVersionRequired()))
dep.GetName(), dep.GetVersion(), existingDep.GetVersion()))
return &cmderrors.LibraryDependenciesResolutionFailedError{Cause: err}
}
}
Expand All @@ -118,16 +123,7 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s
// Find the libReleasesToInstall to install
libReleasesToInstall := map[*librariesindex.Release]*librariesmanager.LibraryInstallPlan{}
installLocation := libraries.FromRPCLibraryInstallLocation(req.GetInstallLocation())
for _, lib := range toInstall {
version, err := parseVersion(lib.GetVersionRequired())
if err != nil {
return err
}
libRelease, err := li.FindRelease(lib.GetName(), version)
if err != nil {
return err
}

for _, libRelease := range toInstall {
installTask, err := lmi.InstallPrerequisiteCheck(libRelease.Library.Name, libRelease.Version, installLocation)
if err != nil {
return err
Expand Down
65 changes: 35 additions & 30 deletions commands/service_library_resolve_deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,43 @@ func (s *arduinoCoreServerImpl) LibraryResolveDependencies(ctx context.Context,
return nil, err
}

return libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetDoNotUpdateInstalledLibraries())
deps, err := libraryResolveDependencies(lme, li, req.GetName(), req.GetVersion(), req.GetDoNotUpdateInstalledLibraries())
if err != nil {
return nil, err
}

// Extract all installed libraries
installedLibs := map[string]*libraries.Library{}
for _, lib := range listLibraries(lme, li, false, false) {
installedLibs[lib.Library.Name] = lib.Library
}

res := []*rpc.LibraryDependencyStatus{}
for _, dep := range deps {
// ...and add information on currently installed versions of the libraries
var installed *semver.Version
required := dep.GetVersion()
if installedLib, has := installedLibs[dep.GetName()]; has {
installed = installedLib.Version
if installed != nil && required != nil && installed.Equal(required) {
// avoid situations like installed=0.53 and required=0.53.0
required = installed
}
}
res = append(res, &rpc.LibraryDependencyStatus{
Name: dep.GetName(),
VersionRequired: required.String(),
VersionInstalled: installed.String(),
})
}
sort.Slice(res, func(i, j int) bool {
return res[i].GetName() < res[j].GetName()
})
return &rpc.LibraryResolveDependenciesResponse{Dependencies: res}, nil
}

func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesindex.Index,
reqName, reqVersion string, noOverwrite bool) (*rpc.LibraryResolveDependenciesResponse, error) {
reqName, reqVersion string, noOverwrite bool) ([]*librariesindex.Release, error) {
version, err := parseVersion(reqVersion)
if err != nil {
return nil, err
Expand All @@ -59,12 +91,6 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
return nil, err
}

// Extract all installed libraries
installedLibs := map[string]*libraries.Library{}
for _, lib := range listLibraries(lme, li, false, false) {
installedLibs[lib.Library.Name] = lib.Library
}

// Resolve all dependencies...
var overrides []*librariesindex.Release
if noOverwrite {
Expand Down Expand Up @@ -92,26 +118,5 @@ func libraryResolveDependencies(lme *librariesmanager.Explorer, li *librariesind
return nil, &cmderrors.LibraryDependenciesResolutionFailedError{}
}

res := []*rpc.LibraryDependencyStatus{}
for _, dep := range deps {
// ...and add information on currently installed versions of the libraries
var installed *semver.Version
required := dep.GetVersion()
if installedLib, has := installedLibs[dep.GetName()]; has {
installed = installedLib.Version
if installed != nil && required != nil && installed.Equal(required) {
// avoid situations like installed=0.53 and required=0.53.0
required = installed
}
}
res = append(res, &rpc.LibraryDependencyStatus{
Name: dep.GetName(),
VersionRequired: required.String(),
VersionInstalled: installed.String(),
})
}
sort.Slice(res, func(i, j int) bool {
return res[i].GetName() < res[j].GetName()
})
return &rpc.LibraryResolveDependenciesResponse{Dependencies: res}, nil
return deps, nil
}
105 changes: 105 additions & 0 deletions commands/service_profile_init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// This file is part of arduino-cli.
//
// Copyright 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 [email protected].

package commands

import (
"context"
"errors"
"fmt"

"github.com/arduino/arduino-cli/commands/cmderrors"
"github.com/arduino/arduino-cli/commands/internal/instances"
"github.com/arduino/arduino-cli/internal/arduino/sketch"
"github.com/arduino/arduino-cli/internal/i18n"
"github.com/arduino/arduino-cli/pkg/fqbn"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
)

// InitProfile creates a new project file if it does not exist. If a profile name with the associated FQBN is specified,
// it is added to the project.
func (s *arduinoCoreServerImpl) InitProfile(ctx context.Context, req *rpc.InitProfileRequest) (*rpc.InitProfileResponse, error) {
// Returns an error if the main file is missing from the sketch so there is no need to check if the path exists
sk, err := sketch.New(paths.New(req.GetSketchPath()))
if err != nil {
return nil, err
}
projectFilePath := sk.GetProjectPath()

if !projectFilePath.Exist() {
err := projectFilePath.WriteFile([]byte("profiles: {}\n"))
if err != nil {
return nil, err
}
}

if req.GetProfileName() != "" {
if req.GetFqbn() == "" {
return nil, &cmderrors.MissingFQBNError{}
}
fqbn, err := fqbn.Parse(req.GetFqbn())
if err != nil {
return nil, &cmderrors.InvalidFQBNError{Cause: err}
}

// Check that the profile name is unique
if profile, _ := sk.GetProfile(req.ProfileName); profile != nil {
return nil, &cmderrors.DuplicateProfileError{Profile: req.ProfileName}
}

pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
if err != nil {
return nil, err
}
defer release()
if pme.Dirty() {
return nil, &cmderrors.InstanceNeedsReinitialization{}
}

// Automatically detect the target platform if it is installed on the user's machine
_, targetPlatform, _, _, _, err := pme.ResolveFQBN(fqbn)
if err != nil {
if targetPlatform == nil {
return nil, &cmderrors.PlatformNotFoundError{
Platform: fmt.Sprintf("%s:%s", fqbn.Vendor, fqbn.Architecture),
Cause: errors.New(i18n.Tr("platform not installed")),
}
}
return nil, &cmderrors.InvalidFQBNError{Cause: err}
}

newProfile := &sketch.Profile{Name: req.GetProfileName(), FQBN: req.GetFqbn()}
// TODO: what to do with the PlatformIndexURL?
newProfile.Platforms = append(newProfile.Platforms, &sketch.ProfilePlatformReference{
Packager: targetPlatform.Platform.Package.Name,
Architecture: targetPlatform.Platform.Architecture,
Version: targetPlatform.Version,
})

sk.Project.Profiles = append(sk.Project.Profiles, newProfile)
// Set the profile as the default one if it's the only one
if req.DefaultProfile || len(sk.Project.Profiles) == 1 {
sk.Project.DefaultProfile = newProfile.Name
}

err = projectFilePath.WriteFile([]byte(sk.Project.AsYaml()))
if err != nil {
return nil, err
}
}

return &rpc.InitProfileResponse{ProjectFilePath: projectFilePath.String()}, nil
}
Loading