Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pre_uninstall scripts #2311

Merged
merged 5 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions arduino/cores/packagemanager/install_uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades(
downloadCB rpc.DownloadProgressCB,
taskCB rpc.TaskProgressCB,
skipPostInstall bool,
skipPreUninstall bool,
) (*cores.PlatformRelease, error) {
if platformRef.PlatformVersion != nil {
return nil, &arduino.InvalidArgumentError{Message: tr("Upgrade doesn't accept parameters with version")}
Expand All @@ -62,7 +63,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades(
if err != nil {
return nil, &arduino.PlatformNotFoundError{Platform: platformRef.String()}
}
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, skipPostInstall); err != nil {
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, skipPostInstall, skipPreUninstall); err != nil {
return nil, err
}

Expand All @@ -75,7 +76,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades(
func (pme *Explorer) DownloadAndInstallPlatformAndTools(
platformRelease *cores.PlatformRelease, requiredTools []*cores.ToolRelease,
downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB,
skipPostInstall bool) error {
skipPostInstall bool, skipPreUninstall bool) error {
log := pme.log.WithField("platform", platformRelease)

// Prerequisite checks before install
Expand Down Expand Up @@ -142,15 +143,15 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(

// If upgrading remove previous release
if installed != nil {
uninstallErr := pme.UninstallPlatform(installed, taskCB)
uninstallErr := pme.UninstallPlatform(installed, taskCB, skipPreUninstall)

// In case of error try to rollback
if uninstallErr != nil {
log.WithError(uninstallErr).Error("Error upgrading platform.")
taskCB(&rpc.TaskProgress{Message: tr("Error upgrading platform: %s", uninstallErr)})

// Rollback
if err := pme.UninstallPlatform(platformRelease, taskCB); err != nil {
if err := pme.UninstallPlatform(platformRelease, taskCB, skipPreUninstall); err != nil {
log.WithError(err).Error("Error rolling-back changes.")
taskCB(&rpc.TaskProgress{Message: tr("Error rolling-back changes: %s", err)})
}
Expand All @@ -162,7 +163,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(
for _, tool := range installedTools {
taskCB(&rpc.TaskProgress{Name: tr("Uninstalling %s, tool is no more required", tool)})
if !pme.IsToolRequired(tool) {
pme.UninstallTool(tool, taskCB)
pme.UninstallTool(tool, taskCB, skipPreUninstall)
}
}

Expand All @@ -175,7 +176,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(
if !platformRelease.IsInstalled() {
return errors.New(tr("platform not installed"))
}
stdout, stderr, err := pme.RunPostInstallScript(platformRelease.InstallDir)
stdout, stderr, err := pme.RunPreOrPostScript(platformRelease.InstallDir, "post_install")
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
if err != nil {
Expand Down Expand Up @@ -229,16 +230,16 @@ func (pme *Explorer) cacheInstalledJSON(platformRelease *cores.PlatformRelease)
return nil
}

// RunPostInstallScript runs the post_install.sh (or post_install.bat) script for the
// specified platformRelease or toolRelease.
func (pme *Explorer) RunPostInstallScript(installDir *paths.Path) ([]byte, []byte, error) {
postInstallFilename := "post_install.sh"
// RunPreOrPostScript runs either the post_install.sh (or post_install.bat) or the pre_uninstall.sh (or pre_uninstall.bat)
// script for the specified platformRelease or toolRelease.
func (pme *Explorer) RunPreOrPostScript(installDir *paths.Path, prefix string) ([]byte, []byte, error) {
scriptFilename := prefix + ".sh"
if runtime.GOOS == "windows" {
postInstallFilename = "post_install.bat"
scriptFilename = prefix + ".bat"
}
postInstall := installDir.Join(postInstallFilename)
if postInstall.Exist() && postInstall.IsNotDir() {
cmd, err := executils.NewProcessFromPath(pme.GetEnvVarsForSpawnedProcess(), postInstall)
script := installDir.Join(scriptFilename)
if script.Exist() && script.IsNotDir() {
cmd, err := executils.NewProcessFromPath(pme.GetEnvVarsForSpawnedProcess(), script)
if err != nil {
return []byte{}, []byte{}, err
}
Expand Down Expand Up @@ -270,7 +271,7 @@ func (pme *Explorer) IsManagedPlatformRelease(platformRelease *cores.PlatformRel
}

// UninstallPlatform remove a PlatformRelease.
func (pme *Explorer) UninstallPlatform(platformRelease *cores.PlatformRelease, taskCB rpc.TaskProgressCB) error {
func (pme *Explorer) UninstallPlatform(platformRelease *cores.PlatformRelease, taskCB rpc.TaskProgressCB, skipPreUninstall bool) error {
log := pme.log.WithField("platform", platformRelease)

log.Info("Uninstalling platform")
Expand All @@ -289,6 +290,20 @@ func (pme *Explorer) UninstallPlatform(platformRelease *cores.PlatformRelease, t
return &arduino.FailedUninstallError{Message: err.Error()}
}

if !skipPreUninstall {
log.Info("Running pre_uninstall script")
taskCB(&rpc.TaskProgress{Message: tr("Running pre_uninstall script.")})
stdout, stderr, err := pme.RunPreOrPostScript(platformRelease.InstallDir, "pre_uninstall")
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
if err != nil {
taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot run pre_uninstall script: %s", err), Completed: true})
}
} else {
log.Info("Skipping pre_uninstall script.")
taskCB(&rpc.TaskProgress{Message: tr("Skipping pre_uninstall script.")})
}

if err := platformRelease.InstallDir.RemoveAll(); err != nil {
err = fmt.Errorf(tr("removing platform files: %s"), err)
log.WithError(err).Error("Error uninstalling")
Expand Down Expand Up @@ -339,7 +354,7 @@ func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.Task
if !skipPostInstall {
log.Info("Running tool post_install script")
taskCB(&rpc.TaskProgress{Message: tr("Configuring tool.")})
stdout, stderr, err := pme.RunPostInstallScript(toolRelease.InstallDir)
stdout, stderr, err := pme.RunPreOrPostScript(toolRelease.InstallDir, "post_install")
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout)})
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr)})
if err != nil {
Expand Down Expand Up @@ -373,7 +388,7 @@ func (pme *Explorer) IsManagedToolRelease(toolRelease *cores.ToolRelease) bool {
}

// UninstallTool remove a ToolRelease.
func (pme *Explorer) UninstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB) error {
func (pme *Explorer) UninstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB, skipPreUninstall bool) error {
log := pme.log.WithField("Tool", toolRelease)
log.Info("Uninstalling tool")

Expand All @@ -388,6 +403,20 @@ func (pme *Explorer) UninstallTool(toolRelease *cores.ToolRelease, taskCB rpc.Ta
return err
}

if !skipPreUninstall {
log.Info("Running pre_uninstall script")
taskCB(&rpc.TaskProgress{Message: tr("Running pre_uninstall script.")})
stdout, stderr, err := pme.RunPreOrPostScript(toolRelease.InstallDir, "pre_uninstall")
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
if err != nil {
taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot run pre_uninstall script: %s", err), Completed: true})
}
} else {
log.Info("Skipping pre_uninstall script.")
taskCB(&rpc.TaskProgress{Message: tr("Skipping pre_uninstall script.")})
}

if err := toolRelease.InstallDir.RemoveAll(); err != nil {
err = &arduino.FailedUninstallError{Message: err.Error()}
log.WithError(err).Error("Error uninstalling")
Expand Down
68 changes: 44 additions & 24 deletions arduino/cores/packagemanager/package_manager_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some code duplication here, this could be an excellent use of subtests

Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
})
}

func TestRunPostInstall(t *testing.T) {
func TestRunScript(t *testing.T) {
pmb := NewBuilder(nil, nil, nil, nil, "test")
pm := pmb.Build()
pme, release := pm.NewExplorer()
Expand All @@ -930,29 +930,49 @@ func TestRunPostInstall(t *testing.T) {
// prepare dummy post install script
dir := paths.New(t.TempDir())

var scriptPath *paths.Path
var err error
if runtime.GOOS == "windows" {
scriptPath = dir.Join("post_install.bat")

err = scriptPath.WriteFile([]byte(
`@echo off
echo sent in stdout
echo sent in stderr 1>&2`))
} else {
scriptPath = dir.Join("post_install.sh")
err = scriptPath.WriteFile([]byte(
`#!/bin/sh
echo "sent in stdout"
echo "sent in stderr" 1>&2`))
type Test struct {
testName string
scriptName string
}

tests := []Test{
{
testName: "PostInstallScript",
scriptName: "post_install",
},
{
testName: "PreUninstallScript",
scriptName: "pre_uninstall",
},
}
require.NoError(t, err)
err = os.Chmod(scriptPath.String(), 0777)
require.NoError(t, err)
stdout, stderr, err := pme.RunPostInstallScript(dir)
require.NoError(t, err)

// `HasPrefix` because windows seem to add a trailing space at the end
require.Equal(t, "sent in stdout", strings.Trim(string(stdout), "\n\r "))
require.Equal(t, "sent in stderr", strings.Trim(string(stderr), "\n\r "))
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
var scriptPath *paths.Path
var err error
if runtime.GOOS == "windows" {
scriptPath = dir.Join(test.scriptName + ".bat")

err = scriptPath.WriteFile([]byte(
`@echo off
echo sent in stdout
echo sent in stderr 1>&2`))
} else {
scriptPath = dir.Join(test.scriptName + ".sh")
err = scriptPath.WriteFile([]byte(
`#!/bin/sh
echo "sent in stdout"
echo "sent in stderr" 1>&2`))
}
require.NoError(t, err)
err = os.Chmod(scriptPath.String(), 0777)
require.NoError(t, err)
stdout, stderr, err := pme.RunPreOrPostScript(dir, test.scriptName)
require.NoError(t, err)

// `HasPrefix` because windows seem to add a trailing space at the end
require.Equal(t, "sent in stdout", strings.Trim(string(stdout), "\n\r "))
require.Equal(t, "sent in stderr", strings.Trim(string(stderr), "\n\r "))
})
}
}
2 changes: 1 addition & 1 deletion commands/core/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func PlatformInstall(ctx context.Context, req *rpc.PlatformInstallRequest, downl
}
}

if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall()); err != nil {
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall()); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions commands/core/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ func platformUninstall(ctx context.Context, req *rpc.PlatformUninstallRequest, t
return &arduino.NotFoundError{Message: tr("Can't find dependencies for platform %s", ref), Cause: err}
}

if err := pme.UninstallPlatform(platform, taskCB); err != nil {
if err := pme.UninstallPlatform(platform, taskCB, req.GetSkipPreUninstall()); err != nil {
return err
}

for _, tool := range tools {
if !pme.IsToolRequired(tool) {
taskCB(&rpc.TaskProgress{Name: tr("Uninstalling %s, tool is no more required", tool)})
pme.UninstallTool(tool, taskCB)
pme.UninstallTool(tool, taskCB, req.GetSkipPreUninstall())
}
}

Expand Down
3 changes: 2 additions & 1 deletion commands/core/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package core

import (
"context"

"github.com/arduino/arduino-cli/arduino/cores"

"github.com/arduino/arduino-cli/arduino"
Expand All @@ -39,7 +40,7 @@ func PlatformUpgrade(ctx context.Context, req *rpc.PlatformUpgradeRequest, downl
Package: req.PlatformPackage,
PlatformArchitecture: req.Architecture,
}
platform, err := pme.DownloadAndInstallPlatformUpgrades(ref, downloadCB, taskCB, req.GetSkipPostInstall())
platform, err := pme.DownloadAndInstallPlatformUpgrades(ref, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall())
if err != nil {
return platform, err
}
Expand Down
18 changes: 18 additions & 0 deletions docs/platform-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -1559,3 +1559,21 @@ software is in use:
- **Arduino CLI**: (since 0.12.0) runs the script for any installed platform when Arduino CLI is in "interactive" mode.
This behavior
[can be configured](https://arduino.github.io/arduino-cli/latest/commands/arduino-cli_core_install/#options)

## Pre-uninstall script

Before Boards Manager starts uninstalling a platform, it checks for the presence of a script named:

- `pre_uninstall.bat` - when running on Windows
- `pre_uninstall.sh` - when running on any non-Windows operating system

If present, the script is executed.

This script may be used to configure the user's system for the removal of drivers, stopping background programs and
execute any action that should be performed before the platform files are removed.

The circumstances under which the pre-uninstall script will run are different depending on which Arduino development
software is in use:

- **Arduino CLI**: runs the script for any installed platform when Arduino CLI is in "interactive" mode. This behavior
[can be configured](https://arduino.github.io/arduino-cli/latest/commands/arduino-cli_core_install/#options)
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,47 @@ import (
"github.com/spf13/cobra"
)

// PostInstallFlags contains flags data used by the core install and the upgrade command
// PrePostScriptsFlags contains flags data used by the core install and the upgrade command
// This is useful so all flags used by commands that need
// this information are consistent with each other.
type PostInstallFlags struct {
runPostInstall bool // force the execution of installation scripts
skipPostInstall bool // skip the execution of installation scripts
type PrePostScriptsFlags struct {
runPostInstall bool // force the execution of installation scripts
skipPostInstall bool // skip the execution of installation scripts
runPreUninstall bool // force the execution of pre uninstall scripts
skipPreUninstall bool // skip the execution of pre uninstall scripts
}

// AddToCommand adds flags that can be used to force running or skipping
// of post installation scripts
func (p *PostInstallFlags) AddToCommand(cmd *cobra.Command) {
func (p *PrePostScriptsFlags) AddToCommand(cmd *cobra.Command) {
cmd.Flags().BoolVar(&p.runPostInstall, "run-post-install", false, tr("Force run of post-install scripts (if the CLI is not running interactively)."))
cmd.Flags().BoolVar(&p.skipPostInstall, "skip-post-install", false, tr("Force skip of post-install scripts (if the CLI is running interactively)."))
cmd.Flags().BoolVar(&p.runPreUninstall, "run-pre-uninstall", false, tr("Force run of pre-uninstall scripts (if the CLI is not running interactively)."))
cmd.Flags().BoolVar(&p.skipPreUninstall, "skip-pre-uninstall", false, tr("Force skip of pre-uninstall scripts (if the CLI is running interactively)."))
}

// GetRunPostInstall returns the run-post-install flag value
func (p *PostInstallFlags) GetRunPostInstall() bool {
func (p *PrePostScriptsFlags) GetRunPostInstall() bool {
return p.runPostInstall
}

// GetSkipPostInstall returns the skip-post-install flag value
func (p *PostInstallFlags) GetSkipPostInstall() bool {
func (p *PrePostScriptsFlags) GetSkipPostInstall() bool {
return p.skipPostInstall
}

// GetRunPreUninstall returns the run-post-install flag value
func (p *PrePostScriptsFlags) GetRunPreUninstall() bool {
return p.runPreUninstall
}

// GetSkipPreUninstall returns the skip-post-install flag value
func (p *PrePostScriptsFlags) GetSkipPreUninstall() bool {
return p.skipPreUninstall
}

// DetectSkipPostInstallValue returns true if a post install script must be run
func (p *PostInstallFlags) DetectSkipPostInstallValue() bool {
func (p *PrePostScriptsFlags) DetectSkipPostInstallValue() bool {
if p.GetRunPostInstall() {
logrus.Info("Will run post-install by user request")
return false
Expand All @@ -64,3 +78,22 @@ func (p *PostInstallFlags) DetectSkipPostInstallValue() bool {
logrus.Info("Running from console, will run post-install by default")
return false
}

// DetectSkipPreUninstallValue returns true if a post install script must be run
func (p *PrePostScriptsFlags) DetectSkipPreUninstallValue() bool {
if p.GetRunPreUninstall() {
logrus.Info("Will run pre-uninstall by user request")
return false
}
if p.GetSkipPreUninstall() {
logrus.Info("Will skip pre-uninstall by user request")
return true
}

if !configuration.IsInteractive {
logrus.Info("Not running from console, will skip pre-uninstall by default")
return true
}
logrus.Info("Running from console, will run pre-uninstall by default")
return false
}
Loading