Skip to content

Commit c570916

Browse files
cmaglieumbynos
andauthored
[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>
1 parent d86bc13 commit c570916

File tree

6 files changed

+230
-189
lines changed

6 files changed

+230
-189
lines changed

arduino/libraries/librariesmanager/install.go

+109-136
Original file line numberDiff line numberDiff line change
@@ -29,72 +29,121 @@ import (
2929
"github.com/arduino/arduino-cli/arduino/utils"
3030
paths "github.com/arduino/go-paths-helper"
3131
"github.com/codeclysm/extract/v3"
32-
"github.com/sirupsen/logrus"
32+
semver "go.bug.st/relaxed-semver"
3333
"gopkg.in/src-d/go-git.v4"
3434
"gopkg.in/src-d/go-git.v4/plumbing"
3535
)
3636

37-
type alreadyInstalledError struct{}
37+
// LibraryInstallPlan contains the main information required to perform a library
38+
// install, like the path where the library should be installed and the library
39+
// that is going to be replaced by the new one.
40+
// This is the result of a call to InstallPrerequisiteCheck.
41+
type LibraryInstallPlan struct {
42+
// Name of the library to install
43+
Name string
3844

39-
func (e *alreadyInstalledError) Error() string {
40-
return tr("library already installed")
41-
}
45+
// Version of the library to install
46+
Version *semver.Version
4247

43-
var (
44-
// ErrAlreadyInstalled is returned when a library is already installed and task
45-
// cannot proceed.
46-
ErrAlreadyInstalled = &alreadyInstalledError{}
47-
)
48+
// TargetPath is the path where the library should be installed.
49+
TargetPath *paths.Path
50+
51+
// ReplacedLib is the library that is going to be replaced by the new one.
52+
ReplacedLib *libraries.Library
53+
54+
// UpToDate is true if the library to install has the same version of the library we are going to replace.
55+
UpToDate bool
56+
}
4857

4958
// InstallPrerequisiteCheck performs prequisite checks to install a library. It returns the
5059
// install path, where the library should be installed and the possible library that is already
5160
// installed on the same folder and it's going to be replaced by the new one.
52-
func (lm *LibrariesManager) InstallPrerequisiteCheck(indexLibrary *librariesindex.Release, installLocation libraries.LibraryLocation) (*paths.Path, *libraries.Library, error) {
53-
installDir := lm.getLibrariesDir(installLocation)
54-
if installDir == nil {
55-
if installLocation == libraries.User {
56-
return nil, nil, fmt.Errorf(tr("User directory not set"))
57-
}
58-
return nil, nil, fmt.Errorf(tr("Builtin libraries directory not set"))
61+
func (lm *LibrariesManager) InstallPrerequisiteCheck(name string, version *semver.Version, installLocation libraries.LibraryLocation) (*LibraryInstallPlan, error) {
62+
installDir, err := lm.getLibrariesDir(installLocation)
63+
if err != nil {
64+
return nil, err
5965
}
6066

61-
name := indexLibrary.Library.Name
6267
libs := lm.FindByReference(&librariesindex.Reference{Name: name}, installLocation)
63-
for _, lib := range libs {
64-
if lib.Version != nil && lib.Version.Equal(indexLibrary.Version) {
65-
return lib.InstallDir, nil, ErrAlreadyInstalled
66-
}
67-
}
6868

6969
if len(libs) > 1 {
7070
libsDir := paths.NewPathList()
7171
for _, lib := range libs {
7272
libsDir.Add(lib.InstallDir)
7373
}
74-
return nil, nil, &arduino.MultipleLibraryInstallDetected{
74+
return nil, &arduino.MultipleLibraryInstallDetected{
7575
LibName: name,
7676
LibsDir: libsDir,
7777
Message: tr("Automatic library install can't be performed in this case, please manually remove all duplicates and retry."),
7878
}
7979
}
8080

8181
var replaced *libraries.Library
82+
var upToDate bool
8283
if len(libs) == 1 {
83-
replaced = libs[0]
84+
lib := libs[0]
85+
replaced = lib
86+
upToDate = lib.Version != nil && lib.Version.Equal(version)
8487
}
8588

86-
libPath := installDir.Join(utils.SanitizeName(indexLibrary.Library.Name))
87-
if replaced != nil && replaced.InstallDir.EquivalentTo(libPath) {
88-
return libPath, replaced, nil
89-
} else if libPath.IsDir() {
90-
return nil, nil, fmt.Errorf(tr("destination dir %s already exists, cannot install"), libPath)
89+
libPath := installDir.Join(utils.SanitizeName(name))
90+
if libPath.IsDir() {
91+
if replaced == nil || !replaced.InstallDir.EquivalentTo(libPath) {
92+
return nil, fmt.Errorf(tr("destination dir %s already exists, cannot install"), libPath)
93+
}
9194
}
92-
return libPath, replaced, nil
95+
96+
return &LibraryInstallPlan{
97+
Name: name,
98+
Version: version,
99+
TargetPath: libPath,
100+
ReplacedLib: replaced,
101+
UpToDate: upToDate,
102+
}, nil
93103
}
94104

95105
// Install installs a library on the specified path.
96-
func (lm *LibrariesManager) Install(indexLibrary *librariesindex.Release, libPath *paths.Path) error {
97-
return indexLibrary.Resource.Install(lm.DownloadsDir, libPath.Parent(), libPath)
106+
func (lm *LibrariesManager) Install(indexLibrary *librariesindex.Release, installPath *paths.Path) error {
107+
return indexLibrary.Resource.Install(lm.DownloadsDir, installPath.Parent(), installPath)
108+
}
109+
110+
// importLibraryFromDirectory installs a library by copying it from the given directory.
111+
func (lm *LibrariesManager) importLibraryFromDirectory(libPath *paths.Path, overwrite bool) error {
112+
// Check if the library is valid and load metatada
113+
if err := validateLibrary(libPath); err != nil {
114+
return err
115+
}
116+
library, err := libraries.Load(libPath, libraries.User)
117+
if err != nil {
118+
return err
119+
}
120+
121+
// Check if the library is already installed and determine install path
122+
installPlan, err := lm.InstallPrerequisiteCheck(library.Name, library.Version, libraries.User)
123+
if err != nil {
124+
return err
125+
}
126+
127+
if installPlan.UpToDate {
128+
if !overwrite {
129+
return fmt.Errorf(tr("library %s already installed"), installPlan.Name)
130+
}
131+
}
132+
if installPlan.ReplacedLib != nil {
133+
if !overwrite {
134+
return fmt.Errorf(tr("Library %[1]s is already installed, but with a different version: %[2]s", installPlan.Name, installPlan.ReplacedLib))
135+
}
136+
if err := lm.Uninstall(installPlan.ReplacedLib); err != nil {
137+
return err
138+
}
139+
}
140+
if installPlan.TargetPath.Exist() {
141+
return fmt.Errorf("%s: %s", tr("destination directory already exists"), installPlan.TargetPath)
142+
}
143+
if err := libPath.CopyDirTo(installPlan.TargetPath); err != nil {
144+
return fmt.Errorf("%s: %w", tr("copying library to destination directory:"), err)
145+
}
146+
return nil
98147
}
99148

100149
// Uninstall removes a Library
@@ -103,7 +152,7 @@ func (lm *LibrariesManager) Uninstall(lib *libraries.Library) error {
103152
return fmt.Errorf(tr("install directory not set"))
104153
}
105154
if err := lib.InstallDir.RemoveAll(); err != nil {
106-
return fmt.Errorf(tr("removing lib directory: %s"), err)
155+
return fmt.Errorf(tr("removing library directory: %s"), err)
107156
}
108157

109158
alternatives := lm.Libraries[lib.Name]
@@ -113,20 +162,15 @@ func (lm *LibrariesManager) Uninstall(lib *libraries.Library) error {
113162
}
114163

115164
// InstallZipLib installs a Zip library on the specified path.
116-
func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath string, overwrite bool) error {
117-
libsDir := lm.getLibrariesDir(libraries.User)
118-
if libsDir == nil {
119-
return fmt.Errorf(tr("User directory not set"))
120-
}
121-
122-
tmpDir, err := paths.MkTempDir(paths.TempDir().String(), "arduino-cli-lib-")
165+
func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath *paths.Path, overwrite bool) error {
166+
// Clone library in a temporary directory
167+
tmpDir, err := paths.MkTempDir("", "")
123168
if err != nil {
124169
return err
125170
}
126-
// Deletes temp dir used to extract archive when finished
127171
defer tmpDir.RemoveAll()
128172

129-
file, err := os.Open(archivePath)
173+
file, err := archivePath.Open()
130174
if err != nil {
131175
return err
132176
}
@@ -138,58 +182,21 @@ func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath strin
138182
return fmt.Errorf(tr("extracting archive: %w"), err)
139183
}
140184

141-
paths, err := tmpDir.ReadDir()
185+
libRootFiles, err := tmpDir.ReadDir()
142186
if err != nil {
143187
return err
144188
}
145-
146-
// Ignores metadata from Mac OS X
147-
paths.FilterOutPrefix("__MACOSX")
148-
149-
if len(paths) > 1 {
189+
libRootFiles.FilterOutPrefix("__MACOSX") // Ignores metadata from Mac OS X
190+
if len(libRootFiles) > 1 {
150191
return fmt.Errorf(tr("archive is not valid: multiple files found in zip file top level"))
151192
}
152-
153-
extractionPath := paths[0]
154-
libraryName := extractionPath.Base()
155-
156-
if err := validateLibrary(extractionPath); err != nil {
157-
return err
158-
}
159-
160-
installPath := libsDir.Join(libraryName)
161-
162-
if err := libsDir.MkdirAll(); err != nil {
163-
return err
164-
}
165-
defer func() {
166-
// Clean up install dir if installation failed
167-
files, err := installPath.ReadDir()
168-
if err == nil && len(files) == 0 {
169-
installPath.RemoveAll()
170-
}
171-
}()
172-
173-
// Delete library folder if already installed
174-
if installPath.IsDir() {
175-
if !overwrite {
176-
return fmt.Errorf(tr("library %s already installed"), libraryName)
177-
}
178-
logrus.
179-
WithField("library name", libraryName).
180-
WithField("install path", installPath).
181-
Trace("Deleting library")
182-
installPath.RemoveAll()
193+
if len(libRootFiles) == 0 {
194+
return fmt.Errorf(tr("archive is not valid: no files found in zip file top level"))
183195
}
196+
tmpInstallPath := libRootFiles[0]
184197

185-
logrus.
186-
WithField("library name", libraryName).
187-
WithField("install path", installPath).
188-
WithField("zip file", archivePath).
189-
Trace("Installing library")
190-
191-
// Copy extracted library in the destination directory
192-
if err := extractionPath.CopyDirTo(installPath); err != nil {
198+
// Install extracted library in the destination directory
199+
if err := lm.importLibraryFromDirectory(tmpInstallPath, overwrite); err != nil {
193200
return fmt.Errorf(tr("moving extracted archive to destination dir: %s"), err)
194201
}
195202

@@ -198,84 +205,50 @@ func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath strin
198205

199206
// InstallGitLib installs a library hosted on a git repository on the specified path.
200207
func (lm *LibrariesManager) InstallGitLib(gitURL string, overwrite bool) error {
201-
installDir := lm.getLibrariesDir(libraries.User)
202-
if installDir == nil {
203-
return fmt.Errorf(tr("User directory not set"))
204-
}
205-
206-
libraryName, ref, err := parseGitURL(gitURL)
208+
gitLibraryName, ref, err := parseGitURL(gitURL)
207209
if err != nil {
208-
logrus.
209-
WithError(err).
210-
Warn("Parsing git URL")
211210
return err
212211
}
213212

214-
// Deletes libraries folder if already installed
215-
installPath := installDir.Join(libraryName)
216-
if installPath.IsDir() {
217-
if !overwrite {
218-
return fmt.Errorf(tr("library %s already installed"), libraryName)
219-
}
220-
logrus.
221-
WithField("library name", libraryName).
222-
WithField("install path", installPath).
223-
Trace("Deleting library")
224-
installPath.RemoveAll()
225-
}
226-
if installPath.Exist() {
227-
return fmt.Errorf(tr("could not create directory %s: a file with the same name exists!", installPath))
213+
// Clone library in a temporary directory
214+
tmp, err := paths.MkTempDir("", "")
215+
if err != nil {
216+
return err
228217
}
229-
230-
logrus.
231-
WithField("library name", libraryName).
232-
WithField("install path", installPath).
233-
WithField("git url", gitURL).
234-
Trace("Installing library")
218+
defer tmp.RemoveAll()
219+
tmpInstallPath := tmp.Join(gitLibraryName)
235220

236221
depth := 1
237222
if ref != "" {
238223
depth = 0
239224
}
240-
repo, err := git.PlainClone(installPath.String(), false, &git.CloneOptions{
225+
repo, err := git.PlainClone(tmpInstallPath.String(), false, &git.CloneOptions{
241226
URL: gitURL,
242227
Depth: depth,
243228
Progress: os.Stdout,
244229
})
245230
if err != nil {
246-
logrus.
247-
WithError(err).
248-
Warn("Cloning git repository")
249231
return err
250232
}
251233

252234
if ref != "" {
253235
if h, err := repo.ResolveRevision(ref); err != nil {
254-
logrus.
255-
WithError(err).
256-
Warnf("Resolving revision %s", ref)
257236
return err
258237
} else if w, err := repo.Worktree(); err != nil {
259-
logrus.
260-
WithError(err).
261-
Warn("Finding worktree")
262238
return err
263239
} else if err := w.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(h.String())}); err != nil {
264-
logrus.
265-
WithError(err).
266-
Warnf("Checking out %s", h)
267240
return err
268241
}
269242
}
270243

271-
if err := validateLibrary(installPath); err != nil {
272-
// Clean up installation directory since this is not a valid library
273-
installPath.RemoveAll()
274-
return err
244+
// We don't want the installed library to be a git repository thus we delete this folder
245+
tmpInstallPath.Join(".git").RemoveAll()
246+
247+
// Install extracted library in the destination directory
248+
if err := lm.importLibraryFromDirectory(tmpInstallPath, overwrite); err != nil {
249+
return fmt.Errorf(tr("moving extracted archive to destination dir: %s"), err)
275250
}
276251

277-
// We don't want the installed library to be a git repository thus we delete this folder
278-
installPath.Join(".git").RemoveAll()
279252
return nil
280253
}
281254

arduino/libraries/librariesmanager/librariesmanager.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package librariesmanager
1717

1818
import (
19+
"errors"
1920
"fmt"
2021
"os"
2122

@@ -140,13 +141,20 @@ func (lm *LibrariesManager) RescanLibraries() []*status.Status {
140141
return statuses
141142
}
142143

143-
func (lm *LibrariesManager) getLibrariesDir(installLocation libraries.LibraryLocation) *paths.Path {
144+
func (lm *LibrariesManager) getLibrariesDir(installLocation libraries.LibraryLocation) (*paths.Path, error) {
144145
for _, dir := range lm.LibrariesDir {
145146
if dir.Location == installLocation {
146-
return dir.Path
147+
return dir.Path, nil
147148
}
148149
}
149-
return nil
150+
switch installLocation {
151+
case libraries.User:
152+
return nil, errors.New(tr("user directory not set"))
153+
case libraries.IDEBuiltIn:
154+
return nil, errors.New(tr("built-in libraries directory not set"))
155+
default:
156+
return nil, fmt.Errorf("libraries directory not set: %s", installLocation.String())
157+
}
150158
}
151159

152160
// LoadLibrariesFromDir loads all libraries in the given directory. Returns

0 commit comments

Comments
 (0)