Skip to content

Commit

Permalink
Add support for pre_uninstall scripts (arduino#2311)
Browse files Browse the repository at this point in the history
* Add skip_pre_uninstall parameter to gRPC platform requests

* Add pre-uninstall flags to CLI arguments

* Run pre-uninstall script when a platform or tool is uninstalled

* Document the changes

* Include pre-uninstall script run into the unit test
  • Loading branch information
MatteoPologruto authored Sep 18, 2023
1 parent f6645a8 commit 29c70df
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 187 deletions.
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
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

0 comments on commit 29c70df

Please sign in to comment.