From 0a87ca2bdac69e917e362e98e16aa90122573a1e Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Wed, 15 May 2024 16:33:10 +0200 Subject: [PATCH 1/5] Wrap v2 tools install function inside tools.Download --- tools/download.go | 88 ++++++----------------------------------------- 1 file changed, 11 insertions(+), 77 deletions(-) diff --git a/tools/download.go b/tools/download.go index 360d6e4c..277bdf2d 100644 --- a/tools/download.go +++ b/tools/download.go @@ -16,24 +16,18 @@ package tools import ( - "bytes" "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" - "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" "runtime" + "github.com/arduino/arduino-create-agent/gen/tools" + "github.com/arduino/arduino-create-agent/utilities" "github.com/arduino/arduino-create-agent/v2/pkgs" - "github.com/arduino/go-paths-helper" "github.com/blang/semver" - "github.com/codeclysm/extract/v3" ) // public vars to allow override in the tests @@ -99,68 +93,21 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { } } - // Download the tool - t.logger("Downloading tool " + name + " from " + correctSystem.URL) - resp, err := http.Get(correctSystem.URL) + tool := pkgs.New(t.index, t.directory.String()) + _, err = tool.Install(context.Background(), &tools.ToolPayload{Name: correctTool.Name, Version: correctTool.Version, Packager: pack}) if err != nil { return err } - defer resp.Body.Close() - // Read the body - body, err = io.ReadAll(resp.Body) + path := filepath.Join(pack, correctTool.Name, correctTool.Version) + safePath, err := utilities.SafeJoin(t.directory.String(), path) if err != nil { return err } - // Checksum - checksum := sha256.Sum256(body) - checkSumString := "SHA-256:" + hex.EncodeToString(checksum[:sha256.Size]) - - if checkSumString != correctSystem.Checksum { - return errors.New("checksum doesn't match") - } - - tempPath := paths.TempDir() - // Create a temporary dir to extract package - if err := tempPath.MkdirAll(); err != nil { - return fmt.Errorf("creating temp dir for extraction: %s", err) - } - tempDir, err := tempPath.MkTempDir("package-") - if err != nil { - return fmt.Errorf("creating temp dir for extraction: %s", err) - } - defer tempDir.RemoveAll() - - t.logger("Unpacking tool " + name) - ctx := context.Background() - reader := bytes.NewReader(body) - // Extract into temp directory - if err := extract.Archive(ctx, reader, tempDir.String(), nil); err != nil { - return fmt.Errorf("extracting archive: %s", err) - } - - location := t.directory.Join(pack, correctTool.Name, correctTool.Version) - err = location.RemoveAll() - if err != nil { - return err - } - - // Check package content and find package root dir - root, err := findPackageRoot(tempDir) - if err != nil { - return fmt.Errorf("searching package root dir: %s", err) - } - - if err := root.Rename(location); err != nil { - if err := root.CopyDirTo(location); err != nil { - return fmt.Errorf("moving extracted archive to destination dir: %s", err) - } - } - // if the tool contains a post_install script, run it: it means it is a tool that needs to install drivers // AFAIK this is only the case for the windows-driver tool - err = t.installDrivers(location.String()) + err = t.installDrivers(safePath) if err != nil { return err } @@ -169,25 +116,12 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { t.logger("Ensure that the files are executable") // Update the tool map - t.logger("Updating map with location " + location.String()) + t.logger("Updating map with location " + safePath) - t.setMapValue(name, location.String()) - t.setMapValue(name+"-"+correctTool.Version, location.String()) - return t.writeMap() -} - -func findPackageRoot(parent *paths.Path) (*paths.Path, error) { - files, err := parent.ReadDir() - if err != nil { - return nil, fmt.Errorf("reading package root dir: %s", err) - } - files.FilterOutPrefix("__MACOSX") + t.setMapValue(name, safePath) + t.setMapValue(name+"-"+version, safePath) - // if there is only one dir, it is the root dir - if len(files) == 1 && files[0].IsDir() { - return files[0], nil - } - return parent, nil + return nil } func findTool(pack, name, version string, data pkgs.Index) (pkgs.Tool, pkgs.System) { From ed8eac622f17d003ad6246f6cbf547d49e76f729 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Wed, 22 May 2024 14:12:54 +0200 Subject: [PATCH 2/5] Download tools defaulting to the replace behaviour --- tools/download.go | 47 ++++--------------------------------------- tools/tools.go | 12 ----------- v2/pkgs/tools.go | 51 ++++++++++++++++++++++++++++++++++------------- 3 files changed, 41 insertions(+), 69 deletions(-) diff --git a/tools/download.go b/tools/download.go index 277bdf2d..95615446 100644 --- a/tools/download.go +++ b/tools/download.go @@ -17,7 +17,6 @@ package tools import ( "context" - "encoding/json" "errors" "os" "os/exec" @@ -36,17 +35,6 @@ var ( Arch = runtime.GOARCH ) -func pathExists(path string) bool { - _, err := os.Stat(path) - if err == nil { - return true - } - if os.IsNotExist(err) { - return false - } - return true -} - // Download will parse the index at the indexURL for the tool to download. // It will extract it in a folder in .arduino-create, and it will update the // Installed map. @@ -62,44 +50,17 @@ func pathExists(path string) bool { // If version is not "latest" and behaviour is "replace", it will download the // version again. If instead behaviour is "keep" it will not download the version // if it already exists. +// +// At the moment the value of behaviour is ignored. func (t *Tools) Download(pack, name, version, behaviour string) error { - body, err := t.index.Read() - if err != nil { - return err - } - - var data pkgs.Index - json.Unmarshal(body, &data) - - // Find the tool by name - correctTool, correctSystem := findTool(pack, name, version, data) - - if correctTool.Name == "" || correctSystem.URL == "" { - t.logger("We couldn't find a tool with the name " + name + " and version " + version + " packaged by " + pack) - return nil - } - - key := correctTool.Name + "-" + correctTool.Version - - // Check if it already exists - if behaviour == "keep" { - location, ok := t.getMapValue(key) - if ok && pathExists(location) { - // overwrite the default tool with this one - t.setMapValue(correctTool.Name, location) - t.logger("The tool is already present on the system") - return t.writeMap() - } - } - tool := pkgs.New(t.index, t.directory.String()) - _, err = tool.Install(context.Background(), &tools.ToolPayload{Name: correctTool.Name, Version: correctTool.Version, Packager: pack}) + _, err := tool.Install(context.Background(), &tools.ToolPayload{Name: name, Version: version, Packager: pack}) if err != nil { return err } - path := filepath.Join(pack, correctTool.Name, correctTool.Version) + path := filepath.Join(pack, name, version) safePath, err := utilities.SafeJoin(t.directory.String(), path) if err != nil { return err diff --git a/tools/tools.go b/tools/tools.go index e641db35..cb9efc78 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -78,18 +78,6 @@ func (t *Tools) getMapValue(key string) (string, bool) { return value, ok } -// writeMap() writes installed map to the json file "installed.json" -func (t *Tools) writeMap() error { - t.mutex.RLock() - defer t.mutex.RUnlock() - b, err := json.Marshal(t.installed) - if err != nil { - return err - } - filePath := t.directory.Join("installed.json") - return filePath.WriteFile(b) -} - // readMap() reads the installed map from json file "installed.json" func (t *Tools) readMap() error { t.mutex.Lock() diff --git a/v2/pkgs/tools.go b/v2/pkgs/tools.go index 55ff6c2e..7510f638 100644 --- a/v2/pkgs/tools.go +++ b/v2/pkgs/tools.go @@ -33,6 +33,7 @@ import ( "github.com/arduino/arduino-create-agent/gen/tools" "github.com/arduino/arduino-create-agent/index" "github.com/arduino/arduino-create-agent/utilities" + "github.com/blang/semver" "github.com/codeclysm/extract/v3" ) @@ -166,20 +167,9 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools var index Index json.Unmarshal(body, &index) - for _, packager := range index.Packages { - if packager.Name != payload.Packager { - continue - } - - for _, tool := range packager.Tools { - if tool.Name == payload.Name && - tool.Version == payload.Version { - - sys := tool.GetFlavourCompatibleWith(runtime.GOOS, runtime.GOARCH) - - return t.install(ctx, path, sys.URL, sys.Checksum) - } - } + correctSystem, found := findTool(payload.Packager, payload.Name, payload.Version, index) + if found { + return t.install(ctx, path, correctSystem.URL, correctSystem.Checksum) } return nil, tools.MakeNotFound( @@ -295,3 +285,36 @@ func writeInstalled(folder, path string) error { return os.WriteFile(installedFile, data, 0644) } + +func findTool(pack, name, version string, data Index) (System, bool) { + var correctTool Tool + correctTool.Version = "0.0" + found := false + + for _, p := range data.Packages { + if p.Name != pack { + continue + } + for _, t := range p.Tools { + if version != "latest" { + if t.Name == name && t.Version == version { + correctTool = t + found = true + } + } else { + // Find latest + v1, _ := semver.Make(t.Version) + v2, _ := semver.Make(correctTool.Version) + if t.Name == name && v1.Compare(v2) > 0 { + correctTool = t + found = true + } + } + } + } + + // Find the url based on system + correctSystem := correctTool.GetFlavourCompatibleWith(runtime.GOOS, runtime.GOARCH) + + return correctSystem, found +} From 887b22bbf9804958b3752002e51f20548892766a Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Wed, 29 May 2024 16:17:48 +0200 Subject: [PATCH 3/5] Improve archive renamer and fix failing tests --- v2/pkgs/tools.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/v2/pkgs/tools.go b/v2/pkgs/tools.go index 7510f638..8cbed5b3 100644 --- a/v2/pkgs/tools.go +++ b/v2/pkgs/tools.go @@ -246,8 +246,11 @@ func (t *Tools) Remove(ctx context.Context, payload *tools.ToolPayload) (*tools. func rename(base string) extract.Renamer { return func(path string) string { parts := strings.Split(filepath.ToSlash(path), "/") - path = strings.Join(parts[1:], "/") - path = filepath.Join(base, path) + newPath := strings.Join(parts[1:], "/") + if newPath == "" { + newPath = filepath.Join(newPath, path) + } + path = filepath.Join(base, newPath) return path } } From a882ed7a2bd6927f97207db591149b7795f757fd Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Wed, 29 May 2024 17:41:08 +0200 Subject: [PATCH 4/5] Find the correct tool and system when `version=latest` is specified --- tools/download.go | 41 ++--------------------------------------- tools/download_test.go | 24 +++++++++++++----------- v2/pkgs/tools.go | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 54 deletions(-) diff --git a/tools/download.go b/tools/download.go index 95615446..df3336d1 100644 --- a/tools/download.go +++ b/tools/download.go @@ -26,13 +26,6 @@ import ( "github.com/arduino/arduino-create-agent/gen/tools" "github.com/arduino/arduino-create-agent/utilities" "github.com/arduino/arduino-create-agent/v2/pkgs" - "github.com/blang/semver" -) - -// public vars to allow override in the tests -var ( - OS = runtime.GOOS - Arch = runtime.GOARCH ) // Download will parse the index at the indexURL for the tool to download. @@ -85,42 +78,12 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { return nil } -func findTool(pack, name, version string, data pkgs.Index) (pkgs.Tool, pkgs.System) { - var correctTool pkgs.Tool - correctTool.Version = "0.0" - - for _, p := range data.Packages { - if p.Name != pack { - continue - } - for _, t := range p.Tools { - if version != "latest" { - if t.Name == name && t.Version == version { - correctTool = t - } - } else { - // Find latest - v1, _ := semver.Make(t.Version) - v2, _ := semver.Make(correctTool.Version) - if t.Name == name && v1.Compare(v2) > 0 { - correctTool = t - } - } - } - } - - // Find the url based on system - correctSystem := correctTool.GetFlavourCompatibleWith(OS, Arch) - - return correctTool, correctSystem -} - func (t *Tools) installDrivers(location string) error { OkPressed := 6 extension := ".bat" // add .\ to force locality preamble := ".\\" - if OS != "windows" { + if runtime.GOOS != "windows" { extension = ".sh" // add ./ to force locality preamble = "./" @@ -132,7 +95,7 @@ func (t *Tools) installDrivers(location string) error { os.Chdir(location) t.logger(preamble + "post_install" + extension) oscmd := exec.Command(preamble + "post_install" + extension) - if OS != "linux" { + if runtime.GOOS != "linux" { // spawning a shell could be the only way to let the user type his password TellCommandNotToSpawnShell(oscmd) } diff --git a/tools/download_test.go b/tools/download_test.go index c45914b5..1e958de9 100644 --- a/tools/download_test.go +++ b/tools/download_test.go @@ -42,8 +42,8 @@ func TestDownloadCorrectPlatform(t *testing.T) { {"linux", "arm", "arm-linux-gnueabihf"}, } defer func() { - OS = runtime.GOOS // restore `runtime.OS` - Arch = runtime.GOARCH // restore `runtime.ARCH` + pkgs.OS = runtime.GOOS // restore `runtime.OS` + pkgs.Arch = runtime.GOARCH // restore `runtime.ARCH` }() testIndex := paths.New("testdata", "test_tool_index.json") buf, err := testIndex.ReadFile() @@ -54,10 +54,11 @@ func TestDownloadCorrectPlatform(t *testing.T) { require.NoError(t, err) for _, tc := range testCases { t.Run(tc.hostOS+tc.hostArch, func(t *testing.T) { - OS = tc.hostOS // override `runtime.OS` for testing purposes - Arch = tc.hostArch // override `runtime.ARCH` for testing purposes + pkgs.OS = tc.hostOS // override `runtime.OS` for testing purposes + pkgs.Arch = tc.hostArch // override `runtime.ARCH` for testing purposes // Find the tool by name - correctTool, correctSystem := findTool("arduino-test", "arduino-fwuploader", "2.2.2", data) + correctTool, correctSystem, found := pkgs.FindTool("arduino-test", "arduino-fwuploader", "2.2.2", data) + require.True(t, found) require.NotNil(t, correctTool) require.NotNil(t, correctSystem) require.Equal(t, correctTool.Name, "arduino-fwuploader") @@ -78,8 +79,8 @@ func TestDownloadFallbackPlatform(t *testing.T) { {"windows", "amd64", "i686-mingw32"}, } defer func() { - OS = runtime.GOOS // restore `runtime.OS` - Arch = runtime.GOARCH // restore `runtime.ARCH` + pkgs.OS = runtime.GOOS // restore `runtime.OS` + pkgs.Arch = runtime.GOARCH // restore `runtime.ARCH` }() testIndex := paths.New("testdata", "test_tool_index.json") buf, err := testIndex.ReadFile() @@ -90,10 +91,11 @@ func TestDownloadFallbackPlatform(t *testing.T) { require.NoError(t, err) for _, tc := range testCases { t.Run(tc.hostOS+tc.hostArch, func(t *testing.T) { - OS = tc.hostOS // override `runtime.OS` for testing purposes - Arch = tc.hostArch // override `runtime.ARCH` for testing purposes + pkgs.OS = tc.hostOS // override `runtime.OS` for testing purposes + pkgs.Arch = tc.hostArch // override `runtime.ARCH` for testing purposes // Find the tool by name - correctTool, correctSystem := findTool("arduino-test", "arduino-fwuploader", "2.2.0", data) + correctTool, correctSystem, found := pkgs.FindTool("arduino-test", "arduino-fwuploader", "2.2.0", data) + require.True(t, found) require.NotNil(t, correctTool) require.NotNil(t, correctSystem) require.Equal(t, correctTool.Name, "arduino-fwuploader") @@ -145,7 +147,7 @@ func TestDownload(t *testing.T) { if filePath.IsDir() { require.DirExists(t, filePath.String()) } else { - if OS == "windows" { + if runtime.GOOS == "windows" { require.FileExists(t, filePath.String()+".exe") } else { require.FileExists(t, filePath.String()) diff --git a/v2/pkgs/tools.go b/v2/pkgs/tools.go index 8cbed5b3..180ab792 100644 --- a/v2/pkgs/tools.go +++ b/v2/pkgs/tools.go @@ -37,6 +37,12 @@ import ( "github.com/codeclysm/extract/v3" ) +// public vars to allow override in the tests +var ( + OS = runtime.GOOS + Arch = runtime.GOARCH +) + // Tools is a client that implements github.com/arduino/arduino-create-agent/gen/tools.Service interface. // It saves tools in a specified folder with this structure: packager/name/version // For example: @@ -167,7 +173,8 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools var index Index json.Unmarshal(body, &index) - correctSystem, found := findTool(payload.Packager, payload.Name, payload.Version, index) + correctTool, correctSystem, found := FindTool(payload.Packager, payload.Name, payload.Version, index) + path = filepath.Join(payload.Packager, correctTool.Name, correctTool.Version) if found { return t.install(ctx, path, correctSystem.URL, correctSystem.Checksum) } @@ -289,7 +296,8 @@ func writeInstalled(folder, path string) error { return os.WriteFile(installedFile, data, 0644) } -func findTool(pack, name, version string, data Index) (System, bool) { +// FindTool searches the index for the correct tool and system that match the specified tool name and version +func FindTool(pack, name, version string, data Index) (Tool, System, bool) { var correctTool Tool correctTool.Version = "0.0" found := false @@ -317,7 +325,7 @@ func findTool(pack, name, version string, data Index) (System, bool) { } // Find the url based on system - correctSystem := correctTool.GetFlavourCompatibleWith(runtime.GOOS, runtime.GOARCH) + correctSystem := correctTool.GetFlavourCompatibleWith(OS, Arch) - return correctSystem, found + return correctTool, correctSystem, found } From 18e9250110c8185a0d4cff95e0f4b9cd0922b280 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Thu, 20 Jun 2024 17:09:00 +0200 Subject: [PATCH 5/5] Reintroduce caching option when downloading tools --- tools/download.go | 4 +-- v2/http.go | 2 +- v2/pkgs/tools.go | 77 +++++++++++++++++++++++++++++++++++-------- v2/pkgs/tools_test.go | 8 ++--- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/tools/download.go b/tools/download.go index df3336d1..6e5fa8b7 100644 --- a/tools/download.go +++ b/tools/download.go @@ -43,11 +43,9 @@ import ( // If version is not "latest" and behaviour is "replace", it will download the // version again. If instead behaviour is "keep" it will not download the version // if it already exists. -// -// At the moment the value of behaviour is ignored. func (t *Tools) Download(pack, name, version, behaviour string) error { - tool := pkgs.New(t.index, t.directory.String()) + tool := pkgs.New(t.index, t.directory.String(), behaviour) _, err := tool.Install(context.Background(), &tools.ToolPayload{Name: name, Version: version, Packager: pack}) if err != nil { return err diff --git a/v2/http.go b/v2/http.go index bcfbc82a..390ec398 100644 --- a/v2/http.go +++ b/v2/http.go @@ -40,7 +40,7 @@ func Server(directory string, index *index.Resource) http.Handler { logAdapter := LogAdapter{Logger: logger} // Mount tools - toolsSvc := pkgs.New(index, directory) + toolsSvc := pkgs.New(index, directory, "replace") toolsEndpoints := toolssvc.NewEndpoints(toolsSvc) toolsServer := toolssvr.New(toolsEndpoints, mux, CustomRequestDecoder, goahttp.ResponseEncoder, errorHandler(logger), nil) toolssvr.Mount(mux, toolsServer) diff --git a/v2/pkgs/tools.go b/v2/pkgs/tools.go index 180ab792..b0daaaae 100644 --- a/v2/pkgs/tools.go +++ b/v2/pkgs/tools.go @@ -57,17 +57,19 @@ var ( // // It requires an Index Resource to search for tools type Tools struct { - index *index.Resource - folder string + index *index.Resource + folder string + behaviour string } // New will return a Tool object, allowing the caller to execute operations on it. // The New function will accept an index as parameter (used to download the indexes) // and a folder used to download the indexes -func New(index *index.Resource, folder string) *Tools { +func New(index *index.Resource, folder, behaviour string) *Tools { return &Tools{ - index: index, - folder: folder, + index: index, + folder: folder, + behaviour: behaviour, } } @@ -175,6 +177,23 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools correctTool, correctSystem, found := FindTool(payload.Packager, payload.Name, payload.Version, index) path = filepath.Join(payload.Packager, correctTool.Name, correctTool.Version) + + key := correctTool.Name + "-" + correctTool.Version + // Check if it already exists + if t.behaviour == "keep" && pathExists(t.folder) { + location, ok, err := checkInstalled(t.folder, key) + if err != nil { + return nil, err + } + if ok && pathExists(location) { + // overwrite the default tool with this one + err := writeInstalled(t.folder, path) + if err != nil { + return nil, err + } + return &tools.Operation{Status: "ok"}, nil + } + } if found { return t.install(ctx, path, correctSystem.URL, correctSystem.Checksum) } @@ -262,21 +281,42 @@ func rename(base string) extract.Renamer { } } -func writeInstalled(folder, path string) error { +func readInstalled(installedFile string) (map[string]string, error) { // read installed.json installed := map[string]string{} - - installedFile, err := utilities.SafeJoin(folder, "installed.json") - if err != nil { - return err - } data, err := os.ReadFile(installedFile) if err == nil { err = json.Unmarshal(data, &installed) if err != nil { - return err + return nil, err } } + return installed, nil +} + +func checkInstalled(folder, key string) (string, bool, error) { + installedFile, err := utilities.SafeJoin(folder, "installed.json") + if err != nil { + return "", false, err + } + installed, err := readInstalled(installedFile) + if err != nil { + return "", false, err + } + location, ok := installed[key] + return location, ok, err +} + +func writeInstalled(folder, path string) error { + // read installed.json + installedFile, err := utilities.SafeJoin(folder, "installed.json") + if err != nil { + return err + } + installed, err := readInstalled(installedFile) + if err != nil { + return err + } parts := strings.Split(path, string(filepath.Separator)) tool := parts[len(parts)-2] @@ -288,7 +328,7 @@ func writeInstalled(folder, path string) error { installed[tool] = toolFile installed[toolWithVersion] = toolFile - data, err = json.Marshal(installed) + data, err := json.Marshal(installed) if err != nil { return err } @@ -296,6 +336,17 @@ func writeInstalled(folder, path string) error { return os.WriteFile(installedFile, data, 0644) } +func pathExists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return true +} + // FindTool searches the index for the correct tool and system that match the specified tool name and version func FindTool(pack, name, version string, data Index) (Tool, System, bool) { var correctTool Tool diff --git a/v2/pkgs/tools_test.go b/v2/pkgs/tools_test.go index 78c56398..be4d5e4d 100644 --- a/v2/pkgs/tools_test.go +++ b/v2/pkgs/tools_test.go @@ -45,7 +45,7 @@ func TestTools(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace") ctx := context.Background() @@ -126,7 +126,7 @@ func TestEvilFilename(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace") ctx := context.Background() @@ -195,7 +195,7 @@ func TestInstalledHead(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace") ctx := context.Background() @@ -216,7 +216,7 @@ func TestInstall(t *testing.T) { LastRefresh: time.Now(), } - tool := pkgs.New(testIndex, tmp) + tool := pkgs.New(testIndex, tmp, "replace") ctx := context.Background()