Unverified Commit c5709162 authored by Cristian Maglie's avatar Cristian Maglie Committed by GitHub

[breaking] Fixed detection of double-install using `lib install` with...

[breaking] Fixed detection of double-install using `lib install` with `--git-url` or `--zip-path` (#1983)

* Remove useless logging

The errors are already reported upstream via returned `err` value

* librariesmanager.InstallPrerequisiteCheck signature change

It now accepts library name and version as single arguments since we are
going to use this function also for libraries not present in the index.

* Added integration test

* Fixed `lib install --git-url` pre-install checks

Now it performs all the needed checks to avoid multiple installations.

* Added test for double install with -.zip-path flag

* Fixed `lib install --zip-path` pre-install checks

Now it performs all the needed checks to avoid multiple installations

* Simplified loop in LibraryInstall function

* Factored some of the checks in LibrariesManager.InstallPrerequisiteCheck

They were duplicated and spread around all the library install
functions.

* Refactored LibrariesManager.getLibrariesDir function

This helped to find out 2 places where the `installDir` was unnecessary.

* Factored all duplicated code for importing a library from a directory

* Updated docs

* Fixed integration test

The installation folder is now taken from the `name` field in `library.properties`.

* Update docs/UPGRADING.md
Co-authored-by: default avatarUmberto Baldi <34278123+umbynos@users.noreply.github.com>
Co-authored-by: default avatarUmberto Baldi <34278123+umbynos@users.noreply.github.com>
parent d86bc138
......@@ -16,6 +16,7 @@
package librariesmanager
import (
"errors"
"fmt"
"os"
......@@ -140,13 +141,20 @@ func (lm *LibrariesManager) RescanLibraries() []*status.Status {
return statuses
}
func (lm *LibrariesManager) getLibrariesDir(installLocation libraries.LibraryLocation) *paths.Path {
func (lm *LibrariesManager) getLibrariesDir(installLocation libraries.LibraryLocation) (*paths.Path, error) {
for _, dir := range lm.LibrariesDir {
if dir.Location == installLocation {
return dir.Path
return dir.Path, nil
}
}
return nil
switch installLocation {
case libraries.User:
return nil, errors.New(tr("user directory not set"))
case libraries.IDEBuiltIn:
return nil, errors.New(tr("built-in libraries directory not set"))
default:
return nil, fmt.Errorf("libraries directory not set: %s", installLocation.String())
}
}
// LoadLibrariesFromDir loads all libraries in the given directory. Returns
......
......@@ -26,6 +26,7 @@ import (
"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
"github.com/arduino/arduino-cli/commands"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
)
......@@ -67,7 +68,7 @@ func LibraryInstall(ctx context.Context, req *rpc.LibraryInstallRequest, downloa
}
// Find the libReleasesToInstall to install
libReleasesToInstall := []*librariesindex.Release{}
libReleasesToInstall := map[*librariesindex.Release]*librariesmanager.LibraryInstallPlan{}
for _, lib := range toInstall {
libRelease, err := findLibraryIndexRelease(lm, &rpc.LibraryInstallRequest{
Name: lib.Name,
......@@ -76,79 +77,55 @@ func LibraryInstall(ctx context.Context, req *rpc.LibraryInstallRequest, downloa
if err != nil {
return err
}
libReleasesToInstall = append(libReleasesToInstall, libRelease)
}
// Check if any of the libraries to install is already installed and remove it from the list
j := 0
for i, libRelease := range libReleasesToInstall {
_, libReplaced, err := lm.InstallPrerequisiteCheck(libRelease, installLocation)
if errors.Is(err, librariesmanager.ErrAlreadyInstalled) {
taskCB(&rpc.TaskProgress{Message: tr("Already installed %s", libRelease), Completed: true})
} else if err != nil {
installTask, err := lm.InstallPrerequisiteCheck(libRelease.Library.Name, libRelease.Version, installLocation)
if err != nil {
return err
} else {
libReleasesToInstall[j] = libReleasesToInstall[i]
j++
}
if installTask.UpToDate {
taskCB(&rpc.TaskProgress{Message: tr("Already installed %s", libRelease), Completed: true})
continue
}
if req.GetNoOverwrite() {
if libReplaced != nil {
return fmt.Errorf(tr("Library %[1]s is already installed, but with a different version: %[2]s", libRelease, libReplaced))
if installTask.ReplacedLib != nil {
return fmt.Errorf(tr("Library %[1]s is already installed, but with a different version: %[2]s", libRelease, installTask.ReplacedLib))
}
}
libReleasesToInstall[libRelease] = installTask
}
libReleasesToInstall = libReleasesToInstall[:j]
didInstall := false
for _, libRelease := range libReleasesToInstall {
for libRelease, installTask := range libReleasesToInstall {
if err := downloadLibrary(lm, libRelease, downloadCB, taskCB); err != nil {
return err
}
if err := installLibrary(lm, libRelease, installLocation, taskCB); err != nil {
if errors.Is(err, librariesmanager.ErrAlreadyInstalled) {
continue
} else {
return err
}
if err := installLibrary(lm, libRelease, installTask, taskCB); err != nil {
return err
}
didInstall = true
}
if didInstall {
if err := commands.Init(&rpc.InitRequest{Instance: req.Instance}, nil); err != nil {
return err
}
if err := commands.Init(&rpc.InitRequest{Instance: req.Instance}, nil); err != nil {
return err
}
return nil
}
func installLibrary(lm *librariesmanager.LibrariesManager, libRelease *librariesindex.Release, installLocation libraries.LibraryLocation, taskCB rpc.TaskProgressCB) error {
func installLibrary(lm *librariesmanager.LibrariesManager, libRelease *librariesindex.Release, installTask *librariesmanager.LibraryInstallPlan, taskCB rpc.TaskProgressCB) error {
taskCB(&rpc.TaskProgress{Name: tr("Installing %s", libRelease)})
logrus.WithField("library", libRelease).Info("Installing library")
libPath, libReplaced, err := lm.InstallPrerequisiteCheck(libRelease, installLocation)
if errors.Is(err, librariesmanager.ErrAlreadyInstalled) {
taskCB(&rpc.TaskProgress{Message: tr("Already installed %s", libRelease), Completed: true})
return err
}
if err != nil {
return &arduino.FailedInstallError{Message: tr("Checking lib install prerequisites"), Cause: err}
}
if libReplaced != nil {
if libReplaced := installTask.ReplacedLib; libReplaced != nil {
taskCB(&rpc.TaskProgress{Message: tr("Replacing %[1]s with %[2]s", libReplaced, libRelease)})
}
if err := lm.Install(libRelease, libPath); err != nil {
return &arduino.FailedLibraryInstallError{Cause: err}
}
if libReplaced != nil && !libReplaced.InstallDir.EquivalentTo(libPath) {
if err := lm.Uninstall(libReplaced); err != nil {
return fmt.Errorf("%s: %s", tr("could not remove old library"), err)
return &arduino.FailedLibraryInstallError{
Cause: fmt.Errorf("%s: %s", tr("could not remove old library"), err)}
}
}
if err := lm.Install(libRelease, installTask.TargetPath); err != nil {
return &arduino.FailedLibraryInstallError{Cause: err}
}
taskCB(&rpc.TaskProgress{Message: tr("Installed %s", libRelease), Completed: true})
return nil
}
......@@ -156,7 +133,7 @@ func installLibrary(lm *librariesmanager.LibrariesManager, libRelease *libraries
// ZipLibraryInstall FIXMEDOC
func ZipLibraryInstall(ctx context.Context, req *rpc.ZipLibraryInstallRequest, taskCB rpc.TaskProgressCB) error {
lm := commands.GetLibraryManager(req)
if err := lm.InstallZipLib(ctx, req.Path, req.Overwrite); err != nil {
if err := lm.InstallZipLib(ctx, paths.New(req.Path), req.Overwrite); err != nil {
return &arduino.FailedLibraryInstallError{Cause: err}
}
taskCB(&rpc.TaskProgress{Message: tr("Library installed"), Completed: true})
......
......@@ -16,6 +16,29 @@ The `sketch.json` file is now completely ignored.
The `cc.arduino.cli.commands.v1.BoardAttach` gRPC command has been removed. This feature is no longer available through
gRPC.
### golang API change in `github.com/arduino/arduino-cli/arduino/libraries/librariesmanager.LibrariesManager`
The following `LibrariesManager.InstallPrerequisiteCheck` methods have changed prototype, from:
```go
func (lm *LibrariesManager) InstallPrerequisiteCheck(indexLibrary *librariesindex.Release, installLocation libraries.LibraryLocation) (*paths.Path, *libraries.Library, error) { ... }
func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath string, overwrite bool) error { ... }
```
to
```go
func (lm *LibrariesManager) InstallPrerequisiteCheck(indexLibrary *librariesindex.Release, installLocation libraries.LibraryLocation) (*paths.Path, *libraries.Library, error) { ... }
func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath *paths.Path, overwrite bool) error { ... }
```
`InstallPrerequisiteCheck` now requires an explicit `name` and `version` instead of a `librariesindex.Release`, because
it can now be used to check any library, not only the libraries available in the index. Also the return value has
changed to a `LibraryInstallPlan` structure, it contains the same information as before (`TargetPath` and `ReplacedLib`)
plus `Name`, `Version`, and an `UpToDate` boolean flag.
`InstallZipLib` method `archivePath` is now a `paths.Path` instead of a `string`.
## 0.29.0
### Removed gRPC API: `cc.arduino.cli.commands.v1.UpdateCoreLibrariesIndex`, `Outdated`, and `Upgrade`
......
......@@ -878,7 +878,7 @@ func TestCompileWithFullyPrecompiledLibrary(t *testing.T) {
require.NoError(t, err)
_, _, err = cli.Run("lib", "install", "--zip-path", wd.Parent().Join("testdata", "Arduino_TensorFlowLite-2.1.0-ALPHA-precompiled.zip").String())
require.NoError(t, err)
sketchFolder := cli.SketchbookDir().Join("libraries", "Arduino_TensorFlowLite-2.1.0-ALPHA-precompiled", "examples", "hello_world")
sketchFolder := cli.SketchbookDir().Join("libraries", "Arduino_TensorFlowLite", "examples", "hello_world")
// Install example dependency
_, _, err = cli.Run("lib", "install", "Arduino_LSM9DS1")
......
......@@ -17,10 +17,13 @@ package lib_test
import (
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"github.com/arduino/arduino-cli/internal/integrationtest"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
"go.bug.st/testifyjson/requirejson"
)
......@@ -145,6 +148,63 @@ func TestDuplicateLibInstallDetection(t *testing.T) {
require.Contains(t, string(stdErr), "The library ArduinoOTA has multiple installations")
}
func TestDuplicateLibInstallFromGitDetection(t *testing.T) {
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()
cliEnv := cli.GetDefaultEnv()
cliEnv["ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL"] = "true"
// Make a double install in the sketchbook/user directory
_, _, err := cli.Run("lib", "install", "Arduino SigFox for MKRFox1200")
require.NoError(t, err)
_, _, err = cli.RunWithCustomEnv(cliEnv, "lib", "install", "--git-url", "https://github.com/arduino-libraries/SigFox#1.0.3")
require.NoError(t, err)
jsonOut, _, err := cli.Run("lib", "list", "--format", "json")
require.NoError(t, err)
// Count how many libraries with the name "Arduino SigFox for MKRFox1200" are installed
requirejson.Parse(t, jsonOut).
Query(`[.[].library.name | select(. == "Arduino SigFox for MKRFox1200")]`).
LengthMustEqualTo(1, "Found multiple installations of Arduino SigFox for MKRFox1200'")
// Try to make a double install by upgrade
_, _, err = cli.Run("lib", "upgrade")
require.NoError(t, err)
// Check if double install happened
jsonOut, _, err = cli.Run("lib", "list", "--format", "json")
require.NoError(t, err)
requirejson.Parse(t, jsonOut).
Query(`[.[].library.name | select(. == "Arduino SigFox for MKRFox1200")]`).
LengthMustEqualTo(1, "Found multiple installations of Arduino SigFox for MKRFox1200'")
// Try to make a double install by zip-installing
tmp, err := paths.MkTempDir("", "")
require.NoError(t, err)
defer tmp.RemoveAll()
tmpZip := tmp.Join("SigFox.zip")
defer tmpZip.Remove()
f, err := tmpZip.Create()
require.NoError(t, err)
resp, err := http.Get("https://github.com/arduino-libraries/SigFox/archive/refs/tags/1.0.3.zip")
require.NoError(t, err)
_, err = io.Copy(f, resp.Body)
require.NoError(t, err)
require.NoError(t, f.Close())
_, _, err = cli.RunWithCustomEnv(cliEnv, "lib", "install", "--zip-path", tmpZip.String())
require.NoError(t, err)
// Check if double install happened
jsonOut, _, err = cli.Run("lib", "list", "--format", "json")
require.NoError(t, err)
requirejson.Parse(t, jsonOut).
Query(`[.[].library.name | select(. == "Arduino SigFox for MKRFox1200")]`).
LengthMustEqualTo(1, "Found multiple installations of Arduino SigFox for MKRFox1200'")
}
func TestLibDepsOutput(t *testing.T) {
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment