Skip to content

Commit

Permalink
Run post_install when needed (#893)
Browse files Browse the repository at this point in the history
* Refactored executils.Command

* Added IsTrusted flag in PlatformReleases

This is required for post_install script processing.

* Run post_install.sh script on trusted platforms

* Small fix on post-install precondition

* Use absolute path for running post_install script

* Added --skip-post-install flags. Refactored PostInstall function.

* Run post-install scripts by default only if the CLI is interactive

This should prevent CI-based runs to block waiting for user interaction.

* Added post_install run messages
  • Loading branch information
cmaglie authored Aug 20, 2020
1 parent 319dede commit 034dd0f
Show file tree
Hide file tree
Showing 26 changed files with 312 additions and 155 deletions.
1 change: 1 addition & 0 deletions arduino/cores/cores.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type PlatformRelease struct {
Menus *properties.Map `json:"-"`
InstallDir *paths.Path `json:"-"`
IsIDEBundled bool `json:"-"`
IsTrusted bool `json:"-"`
}

// BoardManifest contains information about a board. These metadata are usually
Expand Down
28 changes: 23 additions & 5 deletions arduino/cores/packageindex/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import (

"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/resources"
"github.com/arduino/arduino-cli/arduino/security"
"github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
semver "go.bug.st/relaxed-semver"
)

// Index represents Cores and Tools struct as seen from package_index.json file.
type Index struct {
Packages []*indexPackage `json:"packages"`
Packages []*indexPackage `json:"packages"`
IsTrusted bool
}

// indexPackage represents a single entry from package_index.json file.
Expand Down Expand Up @@ -98,11 +101,11 @@ type indexHelp struct {
// with the existing contents of the cores.Packages passed as parameter.
func (index Index) MergeIntoPackages(outPackages cores.Packages) {
for _, inPackage := range index.Packages {
inPackage.extractPackageIn(outPackages)
inPackage.extractPackageIn(outPackages, index.IsTrusted)
}
}

func (inPackage indexPackage) extractPackageIn(outPackages cores.Packages) {
func (inPackage indexPackage) extractPackageIn(outPackages cores.Packages, trusted bool) {
outPackage := outPackages.GetOrCreatePackage(inPackage.Name)
outPackage.Maintainer = inPackage.Maintainer
outPackage.WebsiteURL = inPackage.WebsiteURL
Expand All @@ -115,11 +118,11 @@ func (inPackage indexPackage) extractPackageIn(outPackages cores.Packages) {
}

for _, inPlatform := range inPackage.Platforms {
inPlatform.extractPlatformIn(outPackage)
inPlatform.extractPlatformIn(outPackage, trusted)
}
}

func (inPlatformRelease indexPlatformRelease) extractPlatformIn(outPackage *cores.Package) error {
func (inPlatformRelease indexPlatformRelease) extractPlatformIn(outPackage *cores.Package, trusted bool) error {
outPlatform := outPackage.GetOrCreatePlatform(inPlatformRelease.Architecture)
// FIXME: shall we use the Name and Category of the latest release? or maybe move Name and Category in PlatformRelease?
outPlatform.Name = inPlatformRelease.Name
Expand All @@ -133,6 +136,7 @@ func (inPlatformRelease indexPlatformRelease) extractPlatformIn(outPackage *core
if err != nil {
return fmt.Errorf("creating release: %s", err)
}
outPlatformRelease.IsTrusted = trusted
outPlatformRelease.Resource = &resources.DownloadResource{
ArchiveFileName: inPlatformRelease.ArchiveFileName,
Checksum: inPlatformRelease.Checksum,
Expand Down Expand Up @@ -213,5 +217,19 @@ func LoadIndex(jsonIndexFile *paths.Path) (*Index, error) {
return nil, err
}

jsonSignatureFile := jsonIndexFile.Parent().Join(jsonIndexFile.Base() + ".sig")
trusted, _, err := security.VerifyArduinoDetachedSignature(jsonIndexFile, jsonSignatureFile)
if err != nil {
logrus.
WithField("index", jsonIndexFile).
WithField("signatureFile", jsonSignatureFile).
WithError(err).Infof("Checking signature")
} else {
logrus.
WithField("index", jsonIndexFile).
WithField("signatureFile", jsonSignatureFile).
WithField("trusted", trusted).Infof("Checking signature")
index.IsTrusted = trusted
}
return &index, nil
}
39 changes: 38 additions & 1 deletion arduino/cores/packagemanager/install_uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ package packagemanager

import (
"fmt"
"runtime"

"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/executils"
"github.com/pkg/errors"
)

// InstallPlatform installs a specific release of a platform.
Expand All @@ -28,7 +31,41 @@ func (pm *PackageManager) InstallPlatform(platformRelease *cores.PlatformRelease
"hardware",
platformRelease.Platform.Architecture,
platformRelease.Version.String())
return platformRelease.Resource.Install(pm.DownloadDir, pm.TempDir, destDir)
if err := platformRelease.Resource.Install(pm.DownloadDir, pm.TempDir, destDir); err != nil {
return errors.Errorf("installing platform %s: %s", platformRelease, err)
}
if d, err := destDir.Abs(); err == nil {
platformRelease.InstallDir = d
} else {
return err
}
return nil
}

// RunPostInstallScript runs the post_install.sh (or post_install.bat) script for the
// specified platformRelease.
func (pm *PackageManager) RunPostInstallScript(platformRelease *cores.PlatformRelease) error {
if !platformRelease.IsInstalled() {
return errors.New("platform not installed")
}
postInstallFilename := "post_install.sh"
if runtime.GOOS == "windows" {
postInstallFilename = "post_install.bat"
}
postInstall := platformRelease.InstallDir.Join(postInstallFilename)
if postInstall.Exist() && postInstall.IsNotDir() {
cmd, err := executils.Command(postInstall.String())
if err != nil {
return err
}
cmd.Dir = platformRelease.InstallDir.String()
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}

// IsManagedPlatformRelease returns true if the PlatforRelease is managed by the PackageManager
Expand Down
7 changes: 5 additions & 2 deletions arduino/security/signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import (
"golang.org/x/crypto/openpgp"
)

// VerifyArduinoDetachedSignature that give signaturePath GPG signature match the given targetPath file
// ant the is an authentic signature from Arduino.
// VerifyArduinoDetachedSignature checks that the detached GPG signature (in the
// signaturePath file) matches the given targetPath file and is an authentic
// signature from the bundled trusted keychain. If any of the above conditions
// fails this function returns false. The PGP entity in the trusted keychain that
// produced the signature is returned too.
func VerifyArduinoDetachedSignature(targetPath *paths.Path, signaturePath *paths.Path) (bool, *openpgp.Entity, error) {
keysBox, err := rice.FindBox("keys")
if err != nil {
Expand Down
35 changes: 35 additions & 0 deletions cli/core/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/arduino/arduino-cli/cli/instance"
"github.com/arduino/arduino-cli/cli/output"
"github.com/arduino/arduino-cli/commands/core"
"github.com/arduino/arduino-cli/configuration"
rpc "github.com/arduino/arduino-cli/rpc/commands"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -42,9 +43,42 @@ func initInstallCommand() *cobra.Command {
Args: cobra.MinimumNArgs(1),
Run: runInstallCommand,
}
addPostInstallFlagsToCommand(installCommand)
return installCommand
}

var postInstallFlags struct {
runPostInstall bool
skipPostInstall bool
}

func addPostInstallFlagsToCommand(cmd *cobra.Command) {
cmd.Flags().BoolVar(&postInstallFlags.runPostInstall, "run-post-install", false, "Force run of post-install scripts (if the CLI is not running interactively).")
cmd.Flags().BoolVar(&postInstallFlags.skipPostInstall, "skip-post-install", false, "Force skip of post-install scripts (if the CLI is running interactively).")
}

func detectSkipPostInstallValue() bool {
if postInstallFlags.runPostInstall && postInstallFlags.skipPostInstall {
feedback.Errorf("The flags --run-post-install and --skip-post-install can't be both set at the same time.")
os.Exit(errorcodes.ErrBadArgument)
}
if postInstallFlags.runPostInstall {
logrus.Info("Will run post-install by user request")
return false
}
if postInstallFlags.skipPostInstall {
logrus.Info("Will skip post-install by user request")
return true
}

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

func runInstallCommand(cmd *cobra.Command, args []string) {
inst, err := instance.CreateInstance()
if err != nil {
Expand All @@ -66,6 +100,7 @@ func runInstallCommand(cmd *cobra.Command, args []string) {
PlatformPackage: platformRef.PackageName,
Architecture: platformRef.Architecture,
Version: platformRef.Version,
SkipPostInstall: detectSkipPostInstallValue(),
}
_, err := core.PlatformInstall(context.Background(), platformInstallReq, output.ProgressBar(), output.TaskProgress())
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions cli/core/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func initUpgradeCommand() *cobra.Command {
" " + os.Args[0] + " core upgrade arduino:samd",
Run: runUpgradeCommand,
}
addPostInstallFlagsToCommand(upgradeCommand)
return upgradeCommand
}

Expand Down Expand Up @@ -91,6 +92,7 @@ func runUpgradeCommand(cmd *cobra.Command, args []string) {
Instance: inst,
PlatformPackage: platformRef.PackageName,
Architecture: platformRef.Architecture,
SkipPostInstall: detectSkipPostInstallValue(),
}

_, err := core.PlatformUpgrade(context.Background(), r, output.ProgressBar(), output.TaskProgress())
Expand Down
3 changes: 1 addition & 2 deletions commands/bundled_tools_serial_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ func ListBoards(pm *packagemanager.PackageManager) ([]*BoardPort, error) {
}

// build the command to be executed
args := []string{t.InstallDir.Join("serial-discovery").String()}
cmd, err := executils.Command(args)
cmd, err := executils.Command(t.InstallDir.Join("serial-discovery").String())
if err != nil {
return nil, errors.Wrap(err, "creating discovery process")
}
Expand Down
19 changes: 16 additions & 3 deletions commands/core/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ package core

import (
"context"
"errors"
"fmt"

"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
"github.com/arduino/arduino-cli/commands"
rpc "github.com/arduino/arduino-cli/rpc/commands"
"github.com/pkg/errors"
)

// PlatformInstall FIXMEDOC
Expand All @@ -49,7 +49,7 @@ func PlatformInstall(ctx context.Context, req *rpc.PlatformInstallReq,
return nil, fmt.Errorf("finding platform dependencies: %s", err)
}

err = installPlatform(pm, platform, tools, downloadCB, taskCB)
err = installPlatform(pm, platform, tools, downloadCB, taskCB, req.GetSkipPostInstall())
if err != nil {
return nil, err
}
Expand All @@ -64,7 +64,8 @@ func PlatformInstall(ctx context.Context, req *rpc.PlatformInstallReq,

func installPlatform(pm *packagemanager.PackageManager,
platformRelease *cores.PlatformRelease, requiredTools []*cores.ToolRelease,
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB) error {
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB,
skipPostInstall bool) error {
log := pm.Log.WithField("platform", platformRelease)

// Prerequisite checks before install
Expand Down Expand Up @@ -138,6 +139,18 @@ func installPlatform(pm *packagemanager.PackageManager,
}
}

// Perform post install
if !skipPostInstall && platformRelease.IsTrusted {
log.Info("Running post_install script")
taskCB(&rpc.TaskProgress{Message: "Configuring platform (post_install run)"})
if err := pm.RunPostInstallScript(platformRelease); err != nil {
return errors.Errorf("running post install: %s", err)
}
} else if skipPostInstall {
log.Info("Skipping platform configuration (post_install run).")
taskCB(&rpc.TaskProgress{Message: "Skipping platform configuration (post_install run)"})
}

log.Info("Platform installed")
taskCB(&rpc.TaskProgress{Message: platformRelease.String() + " installed", Completed: true})
return nil
Expand Down
7 changes: 4 additions & 3 deletions commands/core/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func PlatformUpgrade(ctx context.Context, req *rpc.PlatformUpgradeReq,
Package: req.PlatformPackage,
PlatformArchitecture: req.Architecture,
}
if err := upgradePlatform(pm, ref, downloadCB, taskCB); err != nil {
if err := upgradePlatform(pm, ref, downloadCB, taskCB, req.GetSkipPostInstall()); err != nil {
return nil, err
}

Expand All @@ -57,7 +57,8 @@ func PlatformUpgrade(ctx context.Context, req *rpc.PlatformUpgradeReq,
}

func upgradePlatform(pm *packagemanager.PackageManager, platformRef *packagemanager.PlatformReference,
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB) error {
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB,
skipPostInstall bool) error {
if platformRef.PlatformVersion != nil {
return fmt.Errorf("upgrade doesn't accept parameters with version")
}
Expand All @@ -84,7 +85,7 @@ func upgradePlatform(pm *packagemanager.PackageManager, platformRef *packagemana
if err != nil {
return fmt.Errorf("platform %s is not installed", platformRef)
}
err = installPlatform(pm, platform, tools, downloadCB, taskCB)
err = installPlatform(pm, platform, tools, downloadCB, taskCB, skipPostInstall)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion commands/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Debug(ctx context.Context, req *dbg.DebugConfigReq, inStream io.Reader, out
}
entry.Debug("Executing debugger")

cmd, err := executils.Command(commandLine)
cmd, err := executils.Command(commandLine...)
if err != nil {
return nil, errors.Wrap(err, "Cannot execute debug tool")
}
Expand Down
2 changes: 1 addition & 1 deletion commands/upload/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri
if verbose {
outStream.Write([]byte(fmt.Sprintln(cmdLine)))
}
cmd, err := executils.Command(cmdArgs)
cmd, err := executils.Command(cmdArgs...)
if err != nil {
return fmt.Errorf("cannot execute upload tool: %s", err)
}
Expand Down
28 changes: 28 additions & 0 deletions configuration/term.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This file is part of arduino-cli.
//
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package configuration

import (
"os"

"github.com/mattn/go-isatty"
)

// IsInteractive is set to true if the CLI is interactive (it can receive inputs from terminal/console)
var IsInteractive = isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())

// HasConsole is set to true if the CLI outputs to a terminal/console
var HasConsole = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
2 changes: 1 addition & 1 deletion executils/executils.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TellCommandNotToSpawnShell(cmd *exec.Cmd) {
// Command creates a command with the provided command line arguments.
// The first argument is the path to the executable, the remainder are the
// arguments to the command.
func Command(args []string) (*exec.Cmd, error) {
func Command(args ...string) (*exec.Cmd, error) {
if args == nil || len(args) == 0 {
return nil, fmt.Errorf("no executable specified")
}
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ require (
github.com/leonelquinteros/gotext v1.4.0
github.com/marcinbor85/gohex v0.0.0-20200531163658-baab2527a9a2
github.com/mattn/go-colorable v0.1.2
github.com/mattn/go-runewidth v0.0.2 // indirect
github.com/mattn/go-isatty v0.0.8
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.0.5 // indirect
github.com/oleksandr/bonjour v0.0.0-20160508152359-5dcf00d8b228 // indirect
github.com/pkg/errors v0.9.1
Expand Down
Loading

0 comments on commit 034dd0f

Please sign in to comment.