From c570916273a5f9304907fddba9a226245b622e32 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Nov 2022 12:39:00 +0100 Subject: [PATCH] [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: Umberto Baldi <34278123+umbynos@users.noreply.github.com> Co-authored-by: Umberto Baldi <34278123+umbynos@users.noreply.github.com> --- arduino/libraries/librariesmanager/install.go | 245 ++++++++---------- .../librariesmanager/librariesmanager.go | 14 +- commands/lib/install.go | 75 ++---- docs/UPGRADING.md | 23 ++ .../integrationtest/compile_1/compile_test.go | 2 +- internal/integrationtest/lib/lib_test.go | 60 +++++ 6 files changed, 230 insertions(+), 189 deletions(-) diff --git a/arduino/libraries/librariesmanager/install.go b/arduino/libraries/librariesmanager/install.go index 5baa17e3d94..5a3db0ff646 100644 --- a/arduino/libraries/librariesmanager/install.go +++ b/arduino/libraries/librariesmanager/install.go @@ -29,49 +29,49 @@ import ( "github.com/arduino/arduino-cli/arduino/utils" paths "github.com/arduino/go-paths-helper" "github.com/codeclysm/extract/v3" - "github.com/sirupsen/logrus" + semver "go.bug.st/relaxed-semver" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" ) -type alreadyInstalledError struct{} +// LibraryInstallPlan contains the main information required to perform a library +// install, like the path where the library should be installed and the library +// that is going to be replaced by the new one. +// This is the result of a call to InstallPrerequisiteCheck. +type LibraryInstallPlan struct { + // Name of the library to install + Name string -func (e *alreadyInstalledError) Error() string { - return tr("library already installed") -} + // Version of the library to install + Version *semver.Version -var ( - // ErrAlreadyInstalled is returned when a library is already installed and task - // cannot proceed. - ErrAlreadyInstalled = &alreadyInstalledError{} -) + // TargetPath is the path where the library should be installed. + TargetPath *paths.Path + + // ReplacedLib is the library that is going to be replaced by the new one. + ReplacedLib *libraries.Library + + // UpToDate is true if the library to install has the same version of the library we are going to replace. + UpToDate bool +} // InstallPrerequisiteCheck performs prequisite checks to install a library. It returns the // install path, where the library should be installed and the possible library that is already // installed on the same folder and it's going to be replaced by the new one. -func (lm *LibrariesManager) InstallPrerequisiteCheck(indexLibrary *librariesindex.Release, installLocation libraries.LibraryLocation) (*paths.Path, *libraries.Library, error) { - installDir := lm.getLibrariesDir(installLocation) - if installDir == nil { - if installLocation == libraries.User { - return nil, nil, fmt.Errorf(tr("User directory not set")) - } - return nil, nil, fmt.Errorf(tr("Builtin libraries directory not set")) +func (lm *LibrariesManager) InstallPrerequisiteCheck(name string, version *semver.Version, installLocation libraries.LibraryLocation) (*LibraryInstallPlan, error) { + installDir, err := lm.getLibrariesDir(installLocation) + if err != nil { + return nil, err } - name := indexLibrary.Library.Name libs := lm.FindByReference(&librariesindex.Reference{Name: name}, installLocation) - for _, lib := range libs { - if lib.Version != nil && lib.Version.Equal(indexLibrary.Version) { - return lib.InstallDir, nil, ErrAlreadyInstalled - } - } if len(libs) > 1 { libsDir := paths.NewPathList() for _, lib := range libs { libsDir.Add(lib.InstallDir) } - return nil, nil, &arduino.MultipleLibraryInstallDetected{ + return nil, &arduino.MultipleLibraryInstallDetected{ LibName: name, LibsDir: libsDir, Message: tr("Automatic library install can't be performed in this case, please manually remove all duplicates and retry."), @@ -79,22 +79,71 @@ func (lm *LibrariesManager) InstallPrerequisiteCheck(indexLibrary *librariesinde } var replaced *libraries.Library + var upToDate bool if len(libs) == 1 { - replaced = libs[0] + lib := libs[0] + replaced = lib + upToDate = lib.Version != nil && lib.Version.Equal(version) } - libPath := installDir.Join(utils.SanitizeName(indexLibrary.Library.Name)) - if replaced != nil && replaced.InstallDir.EquivalentTo(libPath) { - return libPath, replaced, nil - } else if libPath.IsDir() { - return nil, nil, fmt.Errorf(tr("destination dir %s already exists, cannot install"), libPath) + libPath := installDir.Join(utils.SanitizeName(name)) + if libPath.IsDir() { + if replaced == nil || !replaced.InstallDir.EquivalentTo(libPath) { + return nil, fmt.Errorf(tr("destination dir %s already exists, cannot install"), libPath) + } } - return libPath, replaced, nil + + return &LibraryInstallPlan{ + Name: name, + Version: version, + TargetPath: libPath, + ReplacedLib: replaced, + UpToDate: upToDate, + }, nil } // Install installs a library on the specified path. -func (lm *LibrariesManager) Install(indexLibrary *librariesindex.Release, libPath *paths.Path) error { - return indexLibrary.Resource.Install(lm.DownloadsDir, libPath.Parent(), libPath) +func (lm *LibrariesManager) Install(indexLibrary *librariesindex.Release, installPath *paths.Path) error { + return indexLibrary.Resource.Install(lm.DownloadsDir, installPath.Parent(), installPath) +} + +// importLibraryFromDirectory installs a library by copying it from the given directory. +func (lm *LibrariesManager) importLibraryFromDirectory(libPath *paths.Path, overwrite bool) error { + // Check if the library is valid and load metatada + if err := validateLibrary(libPath); err != nil { + return err + } + library, err := libraries.Load(libPath, libraries.User) + if err != nil { + return err + } + + // Check if the library is already installed and determine install path + installPlan, err := lm.InstallPrerequisiteCheck(library.Name, library.Version, libraries.User) + if err != nil { + return err + } + + if installPlan.UpToDate { + if !overwrite { + return fmt.Errorf(tr("library %s already installed"), installPlan.Name) + } + } + if installPlan.ReplacedLib != nil { + if !overwrite { + return fmt.Errorf(tr("Library %[1]s is already installed, but with a different version: %[2]s", installPlan.Name, installPlan.ReplacedLib)) + } + if err := lm.Uninstall(installPlan.ReplacedLib); err != nil { + return err + } + } + if installPlan.TargetPath.Exist() { + return fmt.Errorf("%s: %s", tr("destination directory already exists"), installPlan.TargetPath) + } + if err := libPath.CopyDirTo(installPlan.TargetPath); err != nil { + return fmt.Errorf("%s: %w", tr("copying library to destination directory:"), err) + } + return nil } // Uninstall removes a Library @@ -103,7 +152,7 @@ func (lm *LibrariesManager) Uninstall(lib *libraries.Library) error { return fmt.Errorf(tr("install directory not set")) } if err := lib.InstallDir.RemoveAll(); err != nil { - return fmt.Errorf(tr("removing lib directory: %s"), err) + return fmt.Errorf(tr("removing library directory: %s"), err) } alternatives := lm.Libraries[lib.Name] @@ -113,20 +162,15 @@ func (lm *LibrariesManager) Uninstall(lib *libraries.Library) error { } // InstallZipLib installs a Zip library on the specified path. -func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath string, overwrite bool) error { - libsDir := lm.getLibrariesDir(libraries.User) - if libsDir == nil { - return fmt.Errorf(tr("User directory not set")) - } - - tmpDir, err := paths.MkTempDir(paths.TempDir().String(), "arduino-cli-lib-") +func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath *paths.Path, overwrite bool) error { + // Clone library in a temporary directory + tmpDir, err := paths.MkTempDir("", "") if err != nil { return err } - // Deletes temp dir used to extract archive when finished defer tmpDir.RemoveAll() - file, err := os.Open(archivePath) + file, err := archivePath.Open() if err != nil { return err } @@ -138,58 +182,21 @@ func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath strin return fmt.Errorf(tr("extracting archive: %w"), err) } - paths, err := tmpDir.ReadDir() + libRootFiles, err := tmpDir.ReadDir() if err != nil { return err } - - // Ignores metadata from Mac OS X - paths.FilterOutPrefix("__MACOSX") - - if len(paths) > 1 { + libRootFiles.FilterOutPrefix("__MACOSX") // Ignores metadata from Mac OS X + if len(libRootFiles) > 1 { return fmt.Errorf(tr("archive is not valid: multiple files found in zip file top level")) } - - extractionPath := paths[0] - libraryName := extractionPath.Base() - - if err := validateLibrary(extractionPath); err != nil { - return err - } - - installPath := libsDir.Join(libraryName) - - if err := libsDir.MkdirAll(); err != nil { - return err - } - defer func() { - // Clean up install dir if installation failed - files, err := installPath.ReadDir() - if err == nil && len(files) == 0 { - installPath.RemoveAll() - } - }() - - // Delete library folder if already installed - if installPath.IsDir() { - if !overwrite { - return fmt.Errorf(tr("library %s already installed"), libraryName) - } - logrus. - WithField("library name", libraryName). - WithField("install path", installPath). - Trace("Deleting library") - installPath.RemoveAll() + if len(libRootFiles) == 0 { + return fmt.Errorf(tr("archive is not valid: no files found in zip file top level")) } + tmpInstallPath := libRootFiles[0] - logrus. - WithField("library name", libraryName). - WithField("install path", installPath). - WithField("zip file", archivePath). - Trace("Installing library") - - // Copy extracted library in the destination directory - if err := extractionPath.CopyDirTo(installPath); err != nil { + // Install extracted library in the destination directory + if err := lm.importLibraryFromDirectory(tmpInstallPath, overwrite); err != nil { return fmt.Errorf(tr("moving extracted archive to destination dir: %s"), err) } @@ -198,84 +205,50 @@ func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath strin // InstallGitLib installs a library hosted on a git repository on the specified path. func (lm *LibrariesManager) InstallGitLib(gitURL string, overwrite bool) error { - installDir := lm.getLibrariesDir(libraries.User) - if installDir == nil { - return fmt.Errorf(tr("User directory not set")) - } - - libraryName, ref, err := parseGitURL(gitURL) + gitLibraryName, ref, err := parseGitURL(gitURL) if err != nil { - logrus. - WithError(err). - Warn("Parsing git URL") return err } - // Deletes libraries folder if already installed - installPath := installDir.Join(libraryName) - if installPath.IsDir() { - if !overwrite { - return fmt.Errorf(tr("library %s already installed"), libraryName) - } - logrus. - WithField("library name", libraryName). - WithField("install path", installPath). - Trace("Deleting library") - installPath.RemoveAll() - } - if installPath.Exist() { - return fmt.Errorf(tr("could not create directory %s: a file with the same name exists!", installPath)) + // Clone library in a temporary directory + tmp, err := paths.MkTempDir("", "") + if err != nil { + return err } - - logrus. - WithField("library name", libraryName). - WithField("install path", installPath). - WithField("git url", gitURL). - Trace("Installing library") + defer tmp.RemoveAll() + tmpInstallPath := tmp.Join(gitLibraryName) depth := 1 if ref != "" { depth = 0 } - repo, err := git.PlainClone(installPath.String(), false, &git.CloneOptions{ + repo, err := git.PlainClone(tmpInstallPath.String(), false, &git.CloneOptions{ URL: gitURL, Depth: depth, Progress: os.Stdout, }) if err != nil { - logrus. - WithError(err). - Warn("Cloning git repository") return err } if ref != "" { if h, err := repo.ResolveRevision(ref); err != nil { - logrus. - WithError(err). - Warnf("Resolving revision %s", ref) return err } else if w, err := repo.Worktree(); err != nil { - logrus. - WithError(err). - Warn("Finding worktree") return err } else if err := w.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(h.String())}); err != nil { - logrus. - WithError(err). - Warnf("Checking out %s", h) return err } } - if err := validateLibrary(installPath); err != nil { - // Clean up installation directory since this is not a valid library - installPath.RemoveAll() - return err + // We don't want the installed library to be a git repository thus we delete this folder + tmpInstallPath.Join(".git").RemoveAll() + + // Install extracted library in the destination directory + if err := lm.importLibraryFromDirectory(tmpInstallPath, overwrite); err != nil { + return fmt.Errorf(tr("moving extracted archive to destination dir: %s"), err) } - // We don't want the installed library to be a git repository thus we delete this folder - installPath.Join(".git").RemoveAll() return nil } diff --git a/arduino/libraries/librariesmanager/librariesmanager.go b/arduino/libraries/librariesmanager/librariesmanager.go index 9291db64117..30dbf69d644 100644 --- a/arduino/libraries/librariesmanager/librariesmanager.go +++ b/arduino/libraries/librariesmanager/librariesmanager.go @@ -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 diff --git a/commands/lib/install.go b/commands/lib/install.go index 0a82c9d074d..f76d77a06d4 100644 --- a/commands/lib/install.go +++ b/commands/lib/install.go @@ -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}) diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index cd8bb72d709..3ab8b55d26a 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -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` diff --git a/internal/integrationtest/compile_1/compile_test.go b/internal/integrationtest/compile_1/compile_test.go index c1f856d071b..7702ba0e4df 100644 --- a/internal/integrationtest/compile_1/compile_test.go +++ b/internal/integrationtest/compile_1/compile_test.go @@ -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") diff --git a/internal/integrationtest/lib/lib_test.go b/internal/integrationtest/lib/lib_test.go index 435d26acf03..388b95abd79 100644 --- a/internal/integrationtest/lib/lib_test.go +++ b/internal/integrationtest/lib/lib_test.go @@ -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()