From 4bbe93bc00e0c651cffd4c4d7f29b94871f49f51 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Oct 2024 08:48:24 +0100 Subject: [PATCH 1/4] Added 'enable_unsafe_install' option for platforms. --- commands/instances.go | 8 +++---- commands/service_library_install.go | 7 +++--- commands/service_platform_install.go | 7 +++++- commands/service_platform_upgrade.go | 7 +++++- .../cores/packagemanager/install_uninstall.go | 22 ++++++++++--------- .../arduino/cores/packagemanager/profiles.go | 4 ++-- internal/arduino/resources/install.go | 21 +++++++++++++----- internal/arduino/resources/install_test.go | 4 ++-- internal/cli/configuration/board_manager.go | 7 ++++++ .../configuration/configuration.schema.json | 4 ++++ internal/cli/configuration/defaults.go | 1 + 11 files changed, 63 insertions(+), 29 deletions(-) diff --git a/commands/instances.go b/commands/instances.go index c208b053c8c..368f54813ce 100644 --- a/commands/instances.go +++ b/commands/instances.go @@ -47,7 +47,7 @@ import ( "google.golang.org/grpc/status" ) -func installTool(ctx context.Context, pm *packagemanager.PackageManager, tool *cores.ToolRelease, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) error { +func installTool(ctx context.Context, pm *packagemanager.PackageManager, tool *cores.ToolRelease, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB, checks resources.IntegrityCheckMode) error { pme, release := pm.NewExplorer() defer release() @@ -56,7 +56,7 @@ func installTool(ctx context.Context, pm *packagemanager.PackageManager, tool *c return errors.New(i18n.Tr("downloading %[1]s tool: %[2]s", tool, err)) } taskCB(&rpc.TaskProgress{Completed: true}) - if err := pme.InstallTool(tool, taskCB, true); err != nil { + if err := pme.InstallTool(tool, taskCB, true, checks); err != nil { return errors.New(i18n.Tr("installing %[1]s tool: %[2]s", tool, err)) } return nil @@ -282,7 +282,7 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor // Install builtin tools if necessary if len(builtinToolsToInstall) > 0 { for _, toolRelease := range builtinToolsToInstall { - if err := installTool(ctx, pmb.Build(), toolRelease, downloadCallback, taskCallback); err != nil { + if err := installTool(ctx, pmb.Build(), toolRelease, downloadCallback, taskCallback, resources.IntegrityCheckFull); err != nil { e := &cmderrors.InitFailedError{ Code: codes.Internal, Cause: err, @@ -394,7 +394,7 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor // Install library taskCallback(&rpc.TaskProgress{Name: i18n.Tr("Installing library %s", libraryRef)}) - if err := libRelease.Resource.Install(pme.DownloadDir, libRoot, libDir); err != nil { + if err := libRelease.Resource.Install(pme.DownloadDir, libRoot, libDir, resources.IntegrityCheckFull); err != nil { taskCallback(&rpc.TaskProgress{Name: i18n.Tr("Error installing library %s", libraryRef)}) e := &cmderrors.FailedLibraryInstallError{Cause: err} responseError(e.GRPCStatus()) diff --git a/commands/service_library_install.go b/commands/service_library_install.go index ca1531ac267..3fe2641282a 100644 --- a/commands/service_library_install.go +++ b/commands/service_library_install.go @@ -25,6 +25,7 @@ import ( "github.com/arduino/arduino-cli/internal/arduino/libraries" "github.com/arduino/arduino-cli/internal/arduino/libraries/librariesindex" "github.com/arduino/arduino-cli/internal/arduino/libraries/librariesmanager" + "github.com/arduino/arduino-cli/internal/arduino/resources" "github.com/arduino/arduino-cli/internal/i18n" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" "github.com/arduino/go-paths-helper" @@ -159,7 +160,7 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s if err := downloadLibrary(ctx, downloadsDir, libRelease, downloadCB, taskCB, downloadReason, s.settings); err != nil { return err } - if err := installLibrary(lmi, downloadsDir, libRelease, installTask, taskCB); err != nil { + if err := installLibrary(lmi, downloadsDir, libRelease, installTask, taskCB, resources.IntegrityCheckFull); err != nil { return err } } @@ -179,7 +180,7 @@ func (s *arduinoCoreServerImpl) LibraryInstall(req *rpc.LibraryInstallRequest, s return nil } -func installLibrary(lmi *librariesmanager.Installer, downloadsDir *paths.Path, libRelease *librariesindex.Release, installTask *librariesmanager.LibraryInstallPlan, taskCB rpc.TaskProgressCB) error { +func installLibrary(lmi *librariesmanager.Installer, downloadsDir *paths.Path, libRelease *librariesindex.Release, installTask *librariesmanager.LibraryInstallPlan, taskCB rpc.TaskProgressCB, checks resources.IntegrityCheckMode) error { taskCB(&rpc.TaskProgress{Name: i18n.Tr("Installing %s", libRelease)}) logrus.WithField("library", libRelease).Info("Installing library") @@ -193,7 +194,7 @@ func installLibrary(lmi *librariesmanager.Installer, downloadsDir *paths.Path, l installPath := installTask.TargetPath tmpDirPath := installPath.Parent() - if err := libRelease.Resource.Install(downloadsDir, tmpDirPath, installPath); err != nil { + if err := libRelease.Resource.Install(downloadsDir, tmpDirPath, installPath, checks); err != nil { return &cmderrors.FailedLibraryInstallError{Cause: err} } diff --git a/commands/service_platform_install.go b/commands/service_platform_install.go index 6a9583fa429..9e268339bc8 100644 --- a/commands/service_platform_install.go +++ b/commands/service_platform_install.go @@ -22,6 +22,7 @@ import ( "github.com/arduino/arduino-cli/commands/cmderrors" "github.com/arduino/arduino-cli/commands/internal/instances" "github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/internal/arduino/resources" "github.com/arduino/arduino-cli/internal/i18n" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" ) @@ -95,7 +96,11 @@ func (s *arduinoCoreServerImpl) PlatformInstall(req *rpc.PlatformInstallRequest, } } - if err := pme.DownloadAndInstallPlatformAndTools(ctx, platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall()); err != nil { + checks := resources.IntegrityCheckFull + if s.settings.BoardManagerEnableUnsafeInstall() { + checks = resources.IntegrityCheckNone + } + if err := pme.DownloadAndInstallPlatformAndTools(ctx, platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall(), checks); err != nil { return err } diff --git a/commands/service_platform_upgrade.go b/commands/service_platform_upgrade.go index 94432e87c31..11aac1346f8 100644 --- a/commands/service_platform_upgrade.go +++ b/commands/service_platform_upgrade.go @@ -21,6 +21,7 @@ import ( "github.com/arduino/arduino-cli/commands/internal/instances" "github.com/arduino/arduino-cli/internal/arduino/cores" "github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/internal/arduino/resources" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" ) @@ -75,7 +76,11 @@ func (s *arduinoCoreServerImpl) PlatformUpgrade(req *rpc.PlatformUpgradeRequest, Package: req.GetPlatformPackage(), PlatformArchitecture: req.GetArchitecture(), } - platform, err := pme.DownloadAndInstallPlatformUpgrades(ctx, ref, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall()) + checks := resources.IntegrityCheckFull + if s.settings.BoardManagerEnableUnsafeInstall() { + checks = resources.IntegrityCheckNone + } + platform, err := pme.DownloadAndInstallPlatformUpgrades(ctx, ref, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall(), checks) if err != nil { return platform, err } diff --git a/internal/arduino/cores/packagemanager/install_uninstall.go b/internal/arduino/cores/packagemanager/install_uninstall.go index 7fb9b44dc02..977e03d8bb0 100644 --- a/internal/arduino/cores/packagemanager/install_uninstall.go +++ b/internal/arduino/cores/packagemanager/install_uninstall.go @@ -25,6 +25,7 @@ import ( "github.com/arduino/arduino-cli/commands/cmderrors" "github.com/arduino/arduino-cli/internal/arduino/cores" "github.com/arduino/arduino-cli/internal/arduino/cores/packageindex" + "github.com/arduino/arduino-cli/internal/arduino/resources" "github.com/arduino/arduino-cli/internal/i18n" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" "github.com/arduino/go-paths-helper" @@ -40,6 +41,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades( taskCB rpc.TaskProgressCB, skipPostInstall bool, skipPreUninstall bool, + checks resources.IntegrityCheckMode, ) (*cores.PlatformRelease, error) { if platformRef.PlatformVersion != nil { return nil, &cmderrors.InvalidArgumentError{Message: i18n.Tr("Upgrade doesn't accept parameters with version")} @@ -64,7 +66,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades( if err != nil { return nil, &cmderrors.PlatformNotFoundError{Platform: platformRef.String()} } - if err := pme.DownloadAndInstallPlatformAndTools(ctx, platformRelease, tools, downloadCB, taskCB, skipPostInstall, skipPreUninstall); err != nil { + if err := pme.DownloadAndInstallPlatformAndTools(ctx, platformRelease, tools, downloadCB, taskCB, skipPostInstall, skipPreUninstall, checks); err != nil { return nil, err } @@ -78,7 +80,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( ctx context.Context, platformRelease *cores.PlatformRelease, requiredTools []*cores.ToolRelease, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB, - skipPostInstall bool, skipPreUninstall bool) error { + skipPostInstall bool, skipPreUninstall bool, checks resources.IntegrityCheckMode) error { log := pme.log.WithField("platform", platformRelease) // Prerequisite checks before install @@ -106,7 +108,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( // Install tools first for _, tool := range toolsToInstall { - if err := pme.InstallTool(tool, taskCB, skipPostInstall); err != nil { + if err := pme.InstallTool(tool, taskCB, skipPostInstall, checks); err != nil { return err } } @@ -138,7 +140,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( } // Install - if err := pme.InstallPlatform(platformRelease); err != nil { + if err := pme.InstallPlatform(platformRelease, checks); err != nil { log.WithError(err).Error("Cannot install platform") return &cmderrors.FailedInstallError{Message: i18n.Tr("Cannot install platform"), Cause: err} } @@ -196,18 +198,18 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( } // InstallPlatform installs a specific release of a platform. -func (pme *Explorer) InstallPlatform(platformRelease *cores.PlatformRelease) error { +func (pme *Explorer) InstallPlatform(platformRelease *cores.PlatformRelease, checks resources.IntegrityCheckMode) error { destDir := pme.PackagesDir.Join( platformRelease.Platform.Package.Name, "hardware", platformRelease.Platform.Architecture, platformRelease.Version.String()) - return pme.InstallPlatformInDirectory(platformRelease, destDir) + return pme.InstallPlatformInDirectory(platformRelease, destDir, checks) } // InstallPlatformInDirectory installs a specific release of a platform in a specific directory. -func (pme *Explorer) InstallPlatformInDirectory(platformRelease *cores.PlatformRelease, destDir *paths.Path) error { - if err := platformRelease.Resource.Install(pme.DownloadDir, pme.tempDir, destDir); err != nil { +func (pme *Explorer) InstallPlatformInDirectory(platformRelease *cores.PlatformRelease, destDir *paths.Path, checks resources.IntegrityCheckMode) error { + if err := platformRelease.Resource.Install(pme.DownloadDir, pme.tempDir, destDir, checks); err != nil { return errors.New(i18n.Tr("installing platform %[1]s: %[2]s", platformRelease, err)) } if d, err := destDir.Abs(); err == nil { @@ -320,7 +322,7 @@ func (pme *Explorer) UninstallPlatform(platformRelease *cores.PlatformRelease, t } // InstallTool installs a specific release of a tool. -func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB, skipPostInstall bool) error { +func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB, skipPostInstall bool, checks resources.IntegrityCheckMode) error { log := pme.log.WithField("Tool", toolRelease) if toolRelease.IsInstalled() { @@ -343,7 +345,7 @@ func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.Task "tools", toolRelease.Tool.Name, toolRelease.Version.String()) - err := toolResource.Install(pme.DownloadDir, pme.tempDir, destDir) + err := toolResource.Install(pme.DownloadDir, pme.tempDir, destDir, checks) if err != nil { log.WithError(err).Warn("Cannot install tool") return &cmderrors.FailedInstallError{Message: i18n.Tr("Cannot install tool %s", toolRelease), Cause: err} diff --git a/internal/arduino/cores/packagemanager/profiles.go b/internal/arduino/cores/packagemanager/profiles.go index f4635950785..d11682b32c0 100644 --- a/internal/arduino/cores/packagemanager/profiles.go +++ b/internal/arduino/cores/packagemanager/profiles.go @@ -132,7 +132,7 @@ func (pmb *Builder) installMissingProfilePlatform(ctx context.Context, platformR // Perform install taskCB(&rpc.TaskProgress{Name: i18n.Tr("Installing platform %s", tmpPlatformRelease)}) - if err := tmpPme.InstallPlatformInDirectory(tmpPlatformRelease, destDir); err != nil { + if err := tmpPme.InstallPlatformInDirectory(tmpPlatformRelease, destDir, resources.IntegrityCheckFull); err != nil { taskCB(&rpc.TaskProgress{Name: i18n.Tr("Error installing platform %s", tmpPlatformRelease)}) return &cmderrors.FailedInstallError{Message: i18n.Tr("Error installing platform %s", tmpPlatformRelease), Cause: err} } @@ -183,7 +183,7 @@ func (pmb *Builder) installMissingProfileTool(ctx context.Context, toolRelease * // Install tool taskCB(&rpc.TaskProgress{Name: i18n.Tr("Installing tool %s", toolRelease)}) - if err := toolResource.Install(pmb.DownloadDir, tmp, destDir); err != nil { + if err := toolResource.Install(pmb.DownloadDir, tmp, destDir, resources.IntegrityCheckFull); err != nil { taskCB(&rpc.TaskProgress{Name: i18n.Tr("Error installing tool %s", toolRelease)}) return &cmderrors.FailedInstallError{Message: i18n.Tr("Error installing tool %s", toolRelease), Cause: err} } diff --git a/internal/arduino/resources/install.go b/internal/arduino/resources/install.go index 0f517af1268..aa9a94e5248 100644 --- a/internal/arduino/resources/install.go +++ b/internal/arduino/resources/install.go @@ -26,18 +26,27 @@ import ( "go.bug.st/cleanup" ) +type IntegrityCheckMode int + +const ( + IntegrityCheckFull IntegrityCheckMode = iota + IntegrityCheckNone +) + // Install installs the resource in three steps: // - the archive is unpacked in a temporary subdir of tempPath // - there should be only one root dir in the unpacked content // - the only root dir is moved/renamed to/as the destination directory // Note that tempPath and destDir must be on the same filesystem partition // otherwise the last step will fail. -func (release *DownloadResource) Install(downloadDir, tempPath, destDir *paths.Path) error { - // Check the integrity of the package - if ok, err := release.TestLocalArchiveIntegrity(downloadDir); err != nil { - return errors.New(i18n.Tr("testing local archive integrity: %s", err)) - } else if !ok { - return errors.New(i18n.Tr("checking local archive integrity")) +func (release *DownloadResource) Install(downloadDir, tempPath, destDir *paths.Path, checks IntegrityCheckMode) error { + if checks != IntegrityCheckNone { + // Check the integrity of the package + if ok, err := release.TestLocalArchiveIntegrity(downloadDir); err != nil { + return errors.New(i18n.Tr("testing local archive integrity: %s", err)) + } else if !ok { + return errors.New(i18n.Tr("checking local archive integrity")) + } } // Create a temporary dir to extract package diff --git a/internal/arduino/resources/install_test.go b/internal/arduino/resources/install_test.go index ce17bcbd293..4db1a9aef22 100644 --- a/internal/arduino/resources/install_test.go +++ b/internal/arduino/resources/install_test.go @@ -44,7 +44,7 @@ func TestInstallPlatform(t *testing.T) { Size: 157, } - require.NoError(t, r.Install(downloadDir, tempPath, destDir)) + require.NoError(t, r.Install(downloadDir, tempPath, destDir, IntegrityCheckFull)) }) tests := []struct { @@ -82,7 +82,7 @@ func TestInstallPlatform(t *testing.T) { require.NoError(t, err) require.NoError(t, os.WriteFile(path.Join(downloadDir.String(), testFileName), origin, 0644)) - err = test.downloadResource.Install(downloadDir, tempPath, destDir) + err = test.downloadResource.Install(downloadDir, tempPath, destDir, IntegrityCheckFull) require.Error(t, err) require.Contains(t, err.Error(), test.error) }) diff --git a/internal/cli/configuration/board_manager.go b/internal/cli/configuration/board_manager.go index 22f982f5404..53f985eef90 100644 --- a/internal/cli/configuration/board_manager.go +++ b/internal/cli/configuration/board_manager.go @@ -21,3 +21,10 @@ func (settings *Settings) BoardManagerAdditionalUrls() []string { } return settings.Defaults.GetStringSlice("board_manager.additional_urls") } + +func (settings *Settings) BoardManagerEnableUnsafeInstall() bool { + if v, ok, _ := settings.GetBoolOk("board_manager.enable_unsafe_install"); ok { + return v + } + return settings.Defaults.GetBool("board_manager.enable_unsafe_install") +} diff --git a/internal/cli/configuration/configuration.schema.json b/internal/cli/configuration/configuration.schema.json index 4d129e5efd8..4ffdb9fafa4 100644 --- a/internal/cli/configuration/configuration.schema.json +++ b/internal/cli/configuration/configuration.schema.json @@ -13,6 +13,10 @@ "type": "string", "format": "uri" } + }, + "enable_unsafe_install": { + "description": "set to `true` to allow installation of packages that do not pass the checksum test. This is considered an unsafe installation method and should be used only for development purposes.", + "type": "boolean" } }, "type": "object" diff --git a/internal/cli/configuration/defaults.go b/internal/cli/configuration/defaults.go index 6e46e2a1564..4a6670f9480 100644 --- a/internal/cli/configuration/defaults.go +++ b/internal/cli/configuration/defaults.go @@ -41,6 +41,7 @@ func SetDefaults(settings *Settings) { // Boards Manager setDefaultValueAndKeyTypeSchema("board_manager.additional_urls", []string{}) + setDefaultValueAndKeyTypeSchema("board_manager.enable_unsafe_install", false) // arduino directories setDefaultValueAndKeyTypeSchema("directories.data", getDefaultArduinoDataDir()) From 2925682be33314041c0120eb960d49ef8e5ac716 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Oct 2024 08:50:44 +0100 Subject: [PATCH 2/4] Updated documentation --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c069638328f..9081bf7c5b9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,6 +2,8 @@ - `board_manager` - `additional_urls` - the URLs to any additional Boards Manager package index files needed for your boards platforms. + - `enable_unsafe_install` - set to `true` to allow installation of packages that do not pass the checksum test. This + is considered an unsafe installation method and should be used only for development purposes. - `daemon` - options related to running Arduino CLI as a [gRPC] server. - `port` - TCP port used for gRPC client connections. - `directories` - directories used by Arduino CLI. From be0b72575e6a9749446796b432406f056a27ce34 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Oct 2024 10:11:27 +0100 Subject: [PATCH 3/4] Do not skip platform index loading if size is invalid or missing --- internal/arduino/cores/packageindex/index.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/arduino/cores/packageindex/index.go b/internal/arduino/cores/packageindex/index.go index fcc892497b3..c2c601c500b 100644 --- a/internal/arduino/cores/packageindex/index.go +++ b/internal/arduino/cores/packageindex/index.go @@ -17,14 +17,12 @@ package packageindex import ( "encoding/json" - "errors" "fmt" "slices" "github.com/arduino/arduino-cli/internal/arduino/cores" "github.com/arduino/arduino-cli/internal/arduino/resources" "github.com/arduino/arduino-cli/internal/arduino/security" - "github.com/arduino/arduino-cli/internal/i18n" "github.com/arduino/go-paths-helper" easyjson "github.com/mailru/easyjson" "github.com/sirupsen/logrus" @@ -273,14 +271,15 @@ func (inPlatformRelease indexPlatformRelease) extractPlatformIn(outPackage *core outPlatform.Deprecated = inPlatformRelease.Deprecated } - size, err := inPlatformRelease.Size.Int64() - if err != nil { - return errors.New(i18n.Tr("invalid platform archive size: %s", err)) - } outPlatformRelease := outPlatform.GetOrCreateRelease(inPlatformRelease.Version) outPlatformRelease.Name = inPlatformRelease.Name outPlatformRelease.Category = inPlatformRelease.Category outPlatformRelease.IsTrusted = trusted + size, err := inPlatformRelease.Size.Int64() + if err != nil { + logrus.Warningf("invalid platform %s archive size: %s", outPlatformRelease, err) + size = 0 + } outPlatformRelease.Resource = &resources.DownloadResource{ ArchiveFileName: inPlatformRelease.ArchiveFileName, Checksum: inPlatformRelease.Checksum, From 4f3d245997e72f73f49236aeabf69f92a0c4762f Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 29 Oct 2024 10:16:26 +0100 Subject: [PATCH 4/4] Added integration test --- internal/integrationtest/core/core_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/integrationtest/core/core_test.go b/internal/integrationtest/core/core_test.go index 635dce891d6..a27b5f6d15a 100644 --- a/internal/integrationtest/core/core_test.go +++ b/internal/integrationtest/core/core_test.go @@ -1366,3 +1366,20 @@ func TestCoreInstallWithWrongArchiveSize(t *testing.T) { _, _, err = cli.Run("--additional-urls", "https://raw.githubusercontent.com/geolink/opentracker-arduino-board/bf6158ebab0402db217bfb02ea61461ddc6f2940/package_opentracker_index.json", "core", "install", "opentracker:sam@1.0.5") require.NoError(t, err) } + +func TestCoreInstallWithMissingOrInvalidChecksumAndUnsafeInstallEnabled(t *testing.T) { + // See: https://github.com/arduino/arduino-cli/issues/1468 + env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t) + defer env.CleanUp() + + _, _, err := cli.Run("--additional-urls", "https://raw.githubusercontent.com/keyboardio/ArduinoCore-GD32-Keyboardio/refs/heads/main/package_gd32_index.json", "core", "update-index") + require.NoError(t, err) + + _, _, err = cli.Run("--additional-urls", "https://raw.githubusercontent.com/keyboardio/ArduinoCore-GD32-Keyboardio/refs/heads/main/package_gd32_index.json", "core", "install", "GD32Community:gd32") + require.Error(t, err) + + _, _, err = cli.RunWithCustomEnv( + map[string]string{"ARDUINO_BOARD_MANAGER_ENABLE_UNSAFE_INSTALL": "true"}, + "--additional-urls", "https://raw.githubusercontent.com/keyboardio/ArduinoCore-GD32-Keyboardio/refs/heads/main/package_gd32_index.json", "core", "install", "GD32Community:gd32") + require.NoError(t, err) +}