Skip to content

Commit 29c70df

Browse files
Add support for pre_uninstall scripts (#2311)
* 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
1 parent f6645a8 commit 29c70df

File tree

13 files changed

+343
-187
lines changed

13 files changed

+343
-187
lines changed

arduino/cores/packagemanager/install_uninstall.go

+46-17
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades(
3838
downloadCB rpc.DownloadProgressCB,
3939
taskCB rpc.TaskProgressCB,
4040
skipPostInstall bool,
41+
skipPreUninstall bool,
4142
) (*cores.PlatformRelease, error) {
4243
if platformRef.PlatformVersion != nil {
4344
return nil, &arduino.InvalidArgumentError{Message: tr("Upgrade doesn't accept parameters with version")}
@@ -62,7 +63,7 @@ func (pme *Explorer) DownloadAndInstallPlatformUpgrades(
6263
if err != nil {
6364
return nil, &arduino.PlatformNotFoundError{Platform: platformRef.String()}
6465
}
65-
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, skipPostInstall); err != nil {
66+
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, skipPostInstall, skipPreUninstall); err != nil {
6667
return nil, err
6768
}
6869

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

8182
// Prerequisite checks before install
@@ -142,15 +143,15 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(
142143

143144
// If upgrading remove previous release
144145
if installed != nil {
145-
uninstallErr := pme.UninstallPlatform(installed, taskCB)
146+
uninstallErr := pme.UninstallPlatform(installed, taskCB, skipPreUninstall)
146147

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

152153
// Rollback
153-
if err := pme.UninstallPlatform(platformRelease, taskCB); err != nil {
154+
if err := pme.UninstallPlatform(platformRelease, taskCB, skipPreUninstall); err != nil {
154155
log.WithError(err).Error("Error rolling-back changes.")
155156
taskCB(&rpc.TaskProgress{Message: tr("Error rolling-back changes: %s", err)})
156157
}
@@ -162,7 +163,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(
162163
for _, tool := range installedTools {
163164
taskCB(&rpc.TaskProgress{Name: tr("Uninstalling %s, tool is no more required", tool)})
164165
if !pme.IsToolRequired(tool) {
165-
pme.UninstallTool(tool, taskCB)
166+
pme.UninstallTool(tool, taskCB, skipPreUninstall)
166167
}
167168
}
168169

@@ -175,7 +176,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools(
175176
if !platformRelease.IsInstalled() {
176177
return errors.New(tr("platform not installed"))
177178
}
178-
stdout, stderr, err := pme.RunPostInstallScript(platformRelease.InstallDir)
179+
stdout, stderr, err := pme.RunPreOrPostScript(platformRelease.InstallDir, "post_install")
179180
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
180181
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
181182
if err != nil {
@@ -229,16 +230,16 @@ func (pme *Explorer) cacheInstalledJSON(platformRelease *cores.PlatformRelease)
229230
return nil
230231
}
231232

232-
// RunPostInstallScript runs the post_install.sh (or post_install.bat) script for the
233-
// specified platformRelease or toolRelease.
234-
func (pme *Explorer) RunPostInstallScript(installDir *paths.Path) ([]byte, []byte, error) {
235-
postInstallFilename := "post_install.sh"
233+
// RunPreOrPostScript runs either the post_install.sh (or post_install.bat) or the pre_uninstall.sh (or pre_uninstall.bat)
234+
// script for the specified platformRelease or toolRelease.
235+
func (pme *Explorer) RunPreOrPostScript(installDir *paths.Path, prefix string) ([]byte, []byte, error) {
236+
scriptFilename := prefix + ".sh"
236237
if runtime.GOOS == "windows" {
237-
postInstallFilename = "post_install.bat"
238+
scriptFilename = prefix + ".bat"
238239
}
239-
postInstall := installDir.Join(postInstallFilename)
240-
if postInstall.Exist() && postInstall.IsNotDir() {
241-
cmd, err := executils.NewProcessFromPath(pme.GetEnvVarsForSpawnedProcess(), postInstall)
240+
script := installDir.Join(scriptFilename)
241+
if script.Exist() && script.IsNotDir() {
242+
cmd, err := executils.NewProcessFromPath(pme.GetEnvVarsForSpawnedProcess(), script)
242243
if err != nil {
243244
return []byte{}, []byte{}, err
244245
}
@@ -270,7 +271,7 @@ func (pme *Explorer) IsManagedPlatformRelease(platformRelease *cores.PlatformRel
270271
}
271272

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

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

293+
if !skipPreUninstall {
294+
log.Info("Running pre_uninstall script")
295+
taskCB(&rpc.TaskProgress{Message: tr("Running pre_uninstall script.")})
296+
stdout, stderr, err := pme.RunPreOrPostScript(platformRelease.InstallDir, "pre_uninstall")
297+
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
298+
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
299+
if err != nil {
300+
taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot run pre_uninstall script: %s", err), Completed: true})
301+
}
302+
} else {
303+
log.Info("Skipping pre_uninstall script.")
304+
taskCB(&rpc.TaskProgress{Message: tr("Skipping pre_uninstall script.")})
305+
}
306+
292307
if err := platformRelease.InstallDir.RemoveAll(); err != nil {
293308
err = fmt.Errorf(tr("removing platform files: %s"), err)
294309
log.WithError(err).Error("Error uninstalling")
@@ -339,7 +354,7 @@ func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.Task
339354
if !skipPostInstall {
340355
log.Info("Running tool post_install script")
341356
taskCB(&rpc.TaskProgress{Message: tr("Configuring tool.")})
342-
stdout, stderr, err := pme.RunPostInstallScript(toolRelease.InstallDir)
357+
stdout, stderr, err := pme.RunPreOrPostScript(toolRelease.InstallDir, "post_install")
343358
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout)})
344359
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr)})
345360
if err != nil {
@@ -373,7 +388,7 @@ func (pme *Explorer) IsManagedToolRelease(toolRelease *cores.ToolRelease) bool {
373388
}
374389

375390
// UninstallTool remove a ToolRelease.
376-
func (pme *Explorer) UninstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB) error {
391+
func (pme *Explorer) UninstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB, skipPreUninstall bool) error {
377392
log := pme.log.WithField("Tool", toolRelease)
378393
log.Info("Uninstalling tool")
379394

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

406+
if !skipPreUninstall {
407+
log.Info("Running pre_uninstall script")
408+
taskCB(&rpc.TaskProgress{Message: tr("Running pre_uninstall script.")})
409+
stdout, stderr, err := pme.RunPreOrPostScript(toolRelease.InstallDir, "pre_uninstall")
410+
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stdout), Completed: true})
411+
skipEmptyMessageTaskProgressCB(taskCB)(&rpc.TaskProgress{Message: string(stderr), Completed: true})
412+
if err != nil {
413+
taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot run pre_uninstall script: %s", err), Completed: true})
414+
}
415+
} else {
416+
log.Info("Skipping pre_uninstall script.")
417+
taskCB(&rpc.TaskProgress{Message: tr("Skipping pre_uninstall script.")})
418+
}
419+
391420
if err := toolRelease.InstallDir.RemoveAll(); err != nil {
392421
err = &arduino.FailedUninstallError{Message: err.Error()}
393422
log.WithError(err).Error("Error uninstalling")

arduino/cores/packagemanager/package_manager_test.go

+44-24
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
921921
})
922922
}
923923

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

933-
var scriptPath *paths.Path
934-
var err error
935-
if runtime.GOOS == "windows" {
936-
scriptPath = dir.Join("post_install.bat")
937-
938-
err = scriptPath.WriteFile([]byte(
939-
`@echo off
940-
echo sent in stdout
941-
echo sent in stderr 1>&2`))
942-
} else {
943-
scriptPath = dir.Join("post_install.sh")
944-
err = scriptPath.WriteFile([]byte(
945-
`#!/bin/sh
946-
echo "sent in stdout"
947-
echo "sent in stderr" 1>&2`))
933+
type Test struct {
934+
testName string
935+
scriptName string
936+
}
937+
938+
tests := []Test{
939+
{
940+
testName: "PostInstallScript",
941+
scriptName: "post_install",
942+
},
943+
{
944+
testName: "PreUninstallScript",
945+
scriptName: "pre_uninstall",
946+
},
948947
}
949-
require.NoError(t, err)
950-
err = os.Chmod(scriptPath.String(), 0777)
951-
require.NoError(t, err)
952-
stdout, stderr, err := pme.RunPostInstallScript(dir)
953-
require.NoError(t, err)
954948

955-
// `HasPrefix` because windows seem to add a trailing space at the end
956-
require.Equal(t, "sent in stdout", strings.Trim(string(stdout), "\n\r "))
957-
require.Equal(t, "sent in stderr", strings.Trim(string(stderr), "\n\r "))
949+
for _, test := range tests {
950+
t.Run(test.testName, func(t *testing.T) {
951+
var scriptPath *paths.Path
952+
var err error
953+
if runtime.GOOS == "windows" {
954+
scriptPath = dir.Join(test.scriptName + ".bat")
955+
956+
err = scriptPath.WriteFile([]byte(
957+
`@echo off
958+
echo sent in stdout
959+
echo sent in stderr 1>&2`))
960+
} else {
961+
scriptPath = dir.Join(test.scriptName + ".sh")
962+
err = scriptPath.WriteFile([]byte(
963+
`#!/bin/sh
964+
echo "sent in stdout"
965+
echo "sent in stderr" 1>&2`))
966+
}
967+
require.NoError(t, err)
968+
err = os.Chmod(scriptPath.String(), 0777)
969+
require.NoError(t, err)
970+
stdout, stderr, err := pme.RunPreOrPostScript(dir, test.scriptName)
971+
require.NoError(t, err)
972+
973+
// `HasPrefix` because windows seem to add a trailing space at the end
974+
require.Equal(t, "sent in stdout", strings.Trim(string(stdout), "\n\r "))
975+
require.Equal(t, "sent in stderr", strings.Trim(string(stderr), "\n\r "))
976+
})
977+
}
958978
}

commands/core/install.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func PlatformInstall(ctx context.Context, req *rpc.PlatformInstallRequest, downl
6363
}
6464
}
6565

66-
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall()); err != nil {
66+
if err := pme.DownloadAndInstallPlatformAndTools(platformRelease, tools, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall()); err != nil {
6767
return err
6868
}
6969

commands/core/uninstall.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ func platformUninstall(ctx context.Context, req *rpc.PlatformUninstallRequest, t
6464
return &arduino.NotFoundError{Message: tr("Can't find dependencies for platform %s", ref), Cause: err}
6565
}
6666

67-
if err := pme.UninstallPlatform(platform, taskCB); err != nil {
67+
if err := pme.UninstallPlatform(platform, taskCB, req.GetSkipPreUninstall()); err != nil {
6868
return err
6969
}
7070

7171
for _, tool := range tools {
7272
if !pme.IsToolRequired(tool) {
7373
taskCB(&rpc.TaskProgress{Name: tr("Uninstalling %s, tool is no more required", tool)})
74-
pme.UninstallTool(tool, taskCB)
74+
pme.UninstallTool(tool, taskCB, req.GetSkipPreUninstall())
7575
}
7676
}
7777

commands/core/upgrade.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package core
1717

1818
import (
1919
"context"
20+
2021
"github.com/arduino/arduino-cli/arduino/cores"
2122

2223
"github.com/arduino/arduino-cli/arduino"
@@ -39,7 +40,7 @@ func PlatformUpgrade(ctx context.Context, req *rpc.PlatformUpgradeRequest, downl
3940
Package: req.PlatformPackage,
4041
PlatformArchitecture: req.Architecture,
4142
}
42-
platform, err := pme.DownloadAndInstallPlatformUpgrades(ref, downloadCB, taskCB, req.GetSkipPostInstall())
43+
platform, err := pme.DownloadAndInstallPlatformUpgrades(ref, downloadCB, taskCB, req.GetSkipPostInstall(), req.GetSkipPreUninstall())
4344
if err != nil {
4445
return platform, err
4546
}

docs/platform-specification.md

+18
Original file line numberDiff line numberDiff line change
@@ -1559,3 +1559,21 @@ software is in use:
15591559
- **Arduino CLI**: (since 0.12.0) runs the script for any installed platform when Arduino CLI is in "interactive" mode.
15601560
This behavior
15611561
[can be configured](https://arduino.github.io/arduino-cli/latest/commands/arduino-cli_core_install/#options)
1562+
1563+
## Pre-uninstall script
1564+
1565+
Before Boards Manager starts uninstalling a platform, it checks for the presence of a script named:
1566+
1567+
- `pre_uninstall.bat` - when running on Windows
1568+
- `pre_uninstall.sh` - when running on any non-Windows operating system
1569+
1570+
If present, the script is executed.
1571+
1572+
This script may be used to configure the user's system for the removal of drivers, stopping background programs and
1573+
execute any action that should be performed before the platform files are removed.
1574+
1575+
The circumstances under which the pre-uninstall script will run are different depending on which Arduino development
1576+
software is in use:
1577+
1578+
- **Arduino CLI**: runs the script for any installed platform when Arduino CLI is in "interactive" mode. This behavior
1579+
[can be configured](https://arduino.github.io/arduino-cli/latest/commands/arduino-cli_core_install/#options)

internal/cli/arguments/post_install.go renamed to internal/cli/arguments/pre_post_script.go

+41-8
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,47 @@ import (
2121
"github.com/spf13/cobra"
2222
)
2323

24-
// PostInstallFlags contains flags data used by the core install and the upgrade command
24+
// PrePostScriptsFlags contains flags data used by the core install and the upgrade command
2525
// This is useful so all flags used by commands that need
2626
// this information are consistent with each other.
27-
type PostInstallFlags struct {
28-
runPostInstall bool // force the execution of installation scripts
29-
skipPostInstall bool // skip the execution of installation scripts
27+
type PrePostScriptsFlags struct {
28+
runPostInstall bool // force the execution of installation scripts
29+
skipPostInstall bool // skip the execution of installation scripts
30+
runPreUninstall bool // force the execution of pre uninstall scripts
31+
skipPreUninstall bool // skip the execution of pre uninstall scripts
3032
}
3133

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

3943
// GetRunPostInstall returns the run-post-install flag value
40-
func (p *PostInstallFlags) GetRunPostInstall() bool {
44+
func (p *PrePostScriptsFlags) GetRunPostInstall() bool {
4145
return p.runPostInstall
4246
}
4347

4448
// GetSkipPostInstall returns the skip-post-install flag value
45-
func (p *PostInstallFlags) GetSkipPostInstall() bool {
49+
func (p *PrePostScriptsFlags) GetSkipPostInstall() bool {
4650
return p.skipPostInstall
4751
}
4852

53+
// GetRunPreUninstall returns the run-post-install flag value
54+
func (p *PrePostScriptsFlags) GetRunPreUninstall() bool {
55+
return p.runPreUninstall
56+
}
57+
58+
// GetSkipPreUninstall returns the skip-post-install flag value
59+
func (p *PrePostScriptsFlags) GetSkipPreUninstall() bool {
60+
return p.skipPreUninstall
61+
}
62+
4963
// DetectSkipPostInstallValue returns true if a post install script must be run
50-
func (p *PostInstallFlags) DetectSkipPostInstallValue() bool {
64+
func (p *PrePostScriptsFlags) DetectSkipPostInstallValue() bool {
5165
if p.GetRunPostInstall() {
5266
logrus.Info("Will run post-install by user request")
5367
return false
@@ -64,3 +78,22 @@ func (p *PostInstallFlags) DetectSkipPostInstallValue() bool {
6478
logrus.Info("Running from console, will run post-install by default")
6579
return false
6680
}
81+
82+
// DetectSkipPreUninstallValue returns true if a post install script must be run
83+
func (p *PrePostScriptsFlags) DetectSkipPreUninstallValue() bool {
84+
if p.GetRunPreUninstall() {
85+
logrus.Info("Will run pre-uninstall by user request")
86+
return false
87+
}
88+
if p.GetSkipPreUninstall() {
89+
logrus.Info("Will skip pre-uninstall by user request")
90+
return true
91+
}
92+
93+
if !configuration.IsInteractive {
94+
logrus.Info("Not running from console, will skip pre-uninstall by default")
95+
return true
96+
}
97+
logrus.Info("Running from console, will run pre-uninstall by default")
98+
return false
99+
}

0 commit comments

Comments
 (0)