diff --git a/arduino/cores/packagemanager/install_uninstall.go b/arduino/cores/packagemanager/install_uninstall.go index 4ab87361111..87beee0a472 100644 --- a/arduino/cores/packagemanager/install_uninstall.go +++ b/arduino/cores/packagemanager/install_uninstall.go @@ -102,7 +102,7 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( // Install tools first for _, tool := range toolsToInstall { - if err := pme.InstallTool(tool, taskCB); err != nil { + if err := pme.InstallTool(tool, taskCB, skipPostInstall); err != nil { return err } } @@ -171,7 +171,10 @@ func (pme *Explorer) DownloadAndInstallPlatformAndTools( if !skipPostInstall { log.Info("Running post_install script") taskCB(&rpc.TaskProgress{Message: tr("Configuring platform.")}) - if err := pme.RunPostInstallScript(platformRelease); err != nil { + if !platformRelease.IsInstalled() { + return errors.New(tr("platform not installed")) + } + if err := pme.RunPostInstallScript(platformRelease.InstallDir); err != nil { taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot configure platform: %s", err)}) } } else { @@ -222,22 +225,19 @@ func (pme *Explorer) cacheInstalledJSON(platformRelease *cores.PlatformRelease) } // RunPostInstallScript runs the post_install.sh (or post_install.bat) script for the -// specified platformRelease. -func (pme *Explorer) RunPostInstallScript(platformRelease *cores.PlatformRelease) error { - if !platformRelease.IsInstalled() { - return errors.New(tr("platform not installed")) - } +// specified platformRelease or toolRelease. +func (pme *Explorer) RunPostInstallScript(installDir *paths.Path) error { postInstallFilename := "post_install.sh" if runtime.GOOS == "windows" { postInstallFilename = "post_install.bat" } - postInstall := platformRelease.InstallDir.Join(postInstallFilename) + postInstall := installDir.Join(postInstallFilename) if postInstall.Exist() && postInstall.IsNotDir() { cmd, err := executils.NewProcessFromPath(pme.GetEnvVarsForSpawnedProcess(), postInstall) if err != nil { return err } - cmd.SetDirFromPath(platformRelease.InstallDir) + cmd.SetDirFromPath(installDir) if err := cmd.Run(); err != nil { return err } @@ -299,7 +299,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) error { +func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.TaskProgressCB, skipPostInstall bool) error { log := pme.log.WithField("Tool", toolRelease) if toolRelease.IsInstalled() { @@ -325,6 +325,22 @@ func (pme *Explorer) InstallTool(toolRelease *cores.ToolRelease, taskCB rpc.Task log.WithError(err).Warn("Cannot install tool") return &arduino.FailedInstallError{Message: tr("Cannot install tool %s", toolRelease), Cause: err} } + if d, err := destDir.Abs(); err == nil { + toolRelease.InstallDir = d + } else { + return err + } + // Perform post install + if !skipPostInstall { + log.Info("Running tool post_install script") + taskCB(&rpc.TaskProgress{Message: tr("Configuring tool.")}) + if err := pme.RunPostInstallScript(toolRelease.InstallDir); err != nil { + taskCB(&rpc.TaskProgress{Message: tr("WARNING cannot configure tool: %s", err)}) + } + } else { + log.Info("Skipping tool configuration.") + taskCB(&rpc.TaskProgress{Message: tr("Skipping tool configuration.")}) + } log.Info("Tool installed") taskCB(&rpc.TaskProgress{Message: tr("%s installed", toolRelease), Completed: true}) diff --git a/commands/instances.go b/commands/instances.go index ddd7a58452e..4bd1cec9e8d 100644 --- a/commands/instances.go +++ b/commands/instances.go @@ -138,7 +138,7 @@ func installTool(pm *packagemanager.PackageManager, tool *cores.ToolRelease, dow return fmt.Errorf(tr("downloading %[1]s tool: %[2]s"), tool, err) } taskCB(&rpc.TaskProgress{Completed: true}) - if err := pme.InstallTool(tool, taskCB); err != nil { + if err := pme.InstallTool(tool, taskCB, true); err != nil { return fmt.Errorf(tr("installing %[1]s tool: %[2]s"), tool, err) } return nil diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 0d867fe05db..5d14fcab452 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -2,6 +2,12 @@ Here you can find a list of migration guides to handle breaking changes between releases of the CLI. +## 0.31.0 + +### Added `post_install` script support for tools + +The `post_install` script now runs when a tool is correctly installed and the CLI is in "interactive" mode. This behavior can be [configured](https://arduino.github.io/arduino-cli/0.30/commands/arduino-cli_core_install/#options). + ## 0.30.0 ### Sketch name validation diff --git a/docs/sketch-specification.md b/docs/sketch-specification.md index 6dddad5d028..105c220cfed 100644 --- a/docs/sketch-specification.md +++ b/docs/sketch-specification.md @@ -35,8 +35,11 @@ The following extensions are supported: - .cpp - C++ files. - .c - C Files. - .S - Assembly language files. -- .h - Header files. -- .tpp, .ipp - Header files (available from Arduino CLI 0.19.0). +- .h, .hpp, .hh [1](#hpp-hh-note) - Header files. +- .tpp, .ipp [2](#tpp-ipp-note) - Header files. + + 1 `.hpp` and `.hh` supported from Arduino IDE 1.8.0/arduino-builder 1.3.22.
+ 2 Supported from Arduino CLI 0.19.0. For information about how each of these files and other parts of the sketch are used during compilation, see the [Sketch build process documentation](sketch-build-process.md). diff --git a/internal/cli/compile/compile.go b/internal/cli/compile/compile.go index cb3c460a46b..318fc021dc3 100644 --- a/internal/cli/compile/compile.go +++ b/internal/cli/compile/compile.go @@ -38,15 +38,36 @@ import ( "github.com/arduino/arduino-cli/table" "github.com/arduino/arduino-cli/version" "github.com/arduino/go-paths-helper" + "github.com/arduino/go-properties-orderedmap" "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +type showPropertiesMode int + +const ( + showPropertiesModeDisabled showPropertiesMode = iota + showPropertiesModeUnexpanded + showPropertiesModeExpanded +) + +func parseShowPropertiesMode(showProperties string) (showPropertiesMode, error) { + val, ok := map[string]showPropertiesMode{ + "disabled": showPropertiesModeDisabled, + "unexpanded": showPropertiesModeUnexpanded, + "expanded": showPropertiesModeExpanded, + }[showProperties] + if !ok { + return showPropertiesModeDisabled, fmt.Errorf(tr("invalid option '%s'.", showProperties)) + } + return val, nil +} + var ( fqbnArg arguments.Fqbn // Fully Qualified Board Name, e.g.: arduino:avr:uno. profileArg arguments.Profile // Profile to use - showProperties bool // Show all build preferences used instead of compiling. + showProperties string // Show all build preferences used instead of compiling. preprocess bool // Print preprocessed code to stdout. buildCachePath string // Builds of 'core.a' are saved into this path to be cached and reused. buildPath string // Path where to save compiled files. @@ -95,7 +116,13 @@ func NewCommand() *cobra.Command { fqbnArg.AddToCommand(compileCommand) profileArg.AddToCommand(compileCommand) compileCommand.Flags().BoolVar(&dumpProfile, "dump-profile", false, tr("Create and print a profile configuration from the build.")) - compileCommand.Flags().BoolVar(&showProperties, "show-properties", false, tr("Show all build properties used instead of compiling.")) + compileCommand.Flags().StringVar( + &showProperties, + "show-properties", + "disabled", + tr(`Show build properties instead of compiling. The properties are returned exactly as they are defined. Use "--show-properties=expanded" to replace placeholders with compilation context values.`), + ) + compileCommand.Flags().Lookup("show-properties").NoOptDefVal = "unexpanded" // default if the flag is present with no value compileCommand.Flags().BoolVar(&preprocess, "preprocess", false, tr("Print preprocessed code to stdout instead of compiling.")) compileCommand.Flags().StringVar(&buildCachePath, "build-cache-path", "", tr("Builds of 'core.a' are saved into this path to be cached and reused.")) compileCommand.Flags().StringVarP(&exportDir, "output-dir", "", "", tr("Save build artifacts in this directory.")) @@ -188,9 +215,14 @@ func runCompileCommand(cmd *cobra.Command, args []string) { overrides = o.Overrides } + showPropertiesM, err := parseShowPropertiesMode(showProperties) + if err != nil { + feedback.Fatal(tr("Error parsing --show-properties flag: %v", err), feedback.ErrGeneric) + } + var stdOut, stdErr io.Writer var stdIORes func() *feedback.OutputStreamsResult - if showProperties { + if showPropertiesM != showPropertiesModeDisabled { stdOut, stdErr, stdIORes = feedback.NewBufferedStreams() } else { stdOut, stdErr, stdIORes = feedback.OutputStreams() @@ -200,7 +232,7 @@ func runCompileCommand(cmd *cobra.Command, args []string) { Instance: inst, Fqbn: fqbn, SketchPath: sketchPath.String(), - ShowProperties: showProperties, + ShowProperties: showPropertiesM != showPropertiesModeDisabled, Preprocess: preprocess, BuildCachePath: buildCachePath, BuildPath: buildPath, @@ -318,7 +350,7 @@ func runCompileCommand(cmd *cobra.Command, args []string) { BuilderResult: compileRes, ProfileOut: profileOut, Success: compileError == nil, - showOnlyProperties: showProperties, + showPropertiesMode: showPropertiesM, } if compileError != nil { @@ -353,9 +385,24 @@ func runCompileCommand(cmd *cobra.Command, args []string) { } feedback.FatalResult(res, feedback.ErrGeneric) } + if showPropertiesM == showPropertiesModeExpanded { + expandPropertiesInResult(res) + } feedback.PrintResult(res) } +func expandPropertiesInResult(res *compileResult) { + expanded, err := properties.LoadFromSlice(res.BuilderResult.GetBuildProperties()) + if err != nil { + res.Error = tr(err.Error()) + } + expandedSlice := make([]string, expanded.Size()) + for i, k := range expanded.Keys() { + expandedSlice[i] = strings.Join([]string{k, expanded.ExpandPropsInString(expanded.Get(k))}, "=") + } + res.BuilderResult.BuildProperties = expandedSlice +} + type compileResult struct { CompilerOut string `json:"compiler_out"` CompilerErr string `json:"compiler_err"` @@ -364,7 +411,7 @@ type compileResult struct { ProfileOut string `json:"profile_out,omitempty"` Error string `json:"error,omitempty"` - showOnlyProperties bool + showPropertiesMode showPropertiesMode } func (r *compileResult) Data() interface{} { @@ -372,8 +419,8 @@ func (r *compileResult) Data() interface{} { } func (r *compileResult) String() string { - if r.showOnlyProperties { - return strings.Join(r.BuilderResult.BuildProperties, fmt.Sprintln()) + if r.showPropertiesMode != showPropertiesModeDisabled { + return strings.Join(r.BuilderResult.GetBuildProperties(), fmt.Sprintln()) } titleColor := color.New(color.FgHiGreen) diff --git a/internal/integrationtest/compile_3/compile_show_properties_test.go b/internal/integrationtest/compile_3/compile_show_properties_test.go index 2aff1164439..780431659d2 100644 --- a/internal/integrationtest/compile_3/compile_show_properties_test.go +++ b/internal/integrationtest/compile_3/compile_show_properties_test.go @@ -16,14 +16,19 @@ package compile_test import ( + "encoding/json" "testing" "github.com/arduino/arduino-cli/internal/integrationtest" + "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" "github.com/arduino/go-properties-orderedmap" "github.com/stretchr/testify/require" - "go.bug.st/testifyjson/requirejson" ) +type cliCompileResponse struct { + BuilderResult *commands.CompileResponse `json:"builder_result"` +} + func TestCompileShowProperties(t *testing.T) { env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t) defer env.CleanUp() @@ -36,17 +41,25 @@ func TestCompileShowProperties(t *testing.T) { bareMinimum := cli.CopySketch("bare_minimum") // Test --show-properties output is clean + // properties are not expanded stdout, stderr, err := cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties", bareMinimum.String()) require.NoError(t, err) - _, err = properties.LoadFromBytes(stdout) + props, err := properties.LoadFromBytes(stdout) require.NoError(t, err, "Output must be a clean property list") require.Empty(t, stderr) + require.True(t, props.ContainsKey("archive_file_path")) + require.Contains(t, props.Get("archive_file_path"), "{build.path}") // Test --show-properties --format JSON output is clean + // properties are not expanded stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties", "--format", "json", bareMinimum.String()) require.NoError(t, err) - requirejson.Parse(t, stdout, "Output must be a valid JSON") require.Empty(t, stderr) + props, err = properties.LoadFromSlice( + requireCompileResponseJson(t, stdout).BuilderResult.GetBuildProperties()) + require.NoError(t, err) + require.True(t, props.ContainsKey("archive_file_path")) + require.Contains(t, props.Get("archive_file_path"), "{build.path}") // Test --show-properties output is clean, with a wrong FQBN stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:unoa", "-v", "--show-properties", bareMinimum.String()) @@ -58,6 +71,54 @@ func TestCompileShowProperties(t *testing.T) { // Test --show-properties --format JSON output is clean, with a wrong FQBN stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:unoa", "-v", "--show-properties", "--format", "json", bareMinimum.String()) require.Error(t, err) - requirejson.Parse(t, stdout, "Output must be a valid JSON") require.Empty(t, stderr) + requireCompileResponseJson(t, stdout) + + // Test --show-properties=unexpanded output is clean + // properties are not expanded + stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties=unexpanded", bareMinimum.String()) + require.NoError(t, err) + props, err = properties.LoadFromBytes(stdout) + require.NoError(t, err, "Output must be a clean property list") + require.Empty(t, stderr) + require.True(t, props.ContainsKey("archive_file_path")) + require.Contains(t, props.Get("archive_file_path"), "{build.path}") + + // Test --show-properties=unexpanded output is clean + // properties are not expanded + stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties=unexpanded", "--format", "json", bareMinimum.String()) + require.NoError(t, err) + require.Empty(t, stderr) + props, err = properties.LoadFromSlice( + requireCompileResponseJson(t, stdout).BuilderResult.GetBuildProperties()) + require.NoError(t, err) + require.True(t, props.ContainsKey("archive_file_path")) + require.Contains(t, props.Get("archive_file_path"), "{build.path}") + + // Test --show-properties=expanded output is clean + // properties are expanded + stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties=expanded", bareMinimum.String()) + require.NoError(t, err) + props, err = properties.LoadFromBytes(stdout) + require.NoError(t, err, "Output must be a clean property list") + require.Empty(t, stderr) + require.True(t, props.ContainsKey("archive_file_path")) + require.NotContains(t, props.Get("archive_file_path"), "{build.path}") + + // Test --show-properties=expanded --format JSON output is clean + // properties are expanded + stdout, stderr, err = cli.Run("compile", "--fqbn", "arduino:avr:uno", "-v", "--show-properties=expanded", "--format", "json", bareMinimum.String()) + require.NoError(t, err) + require.Empty(t, stderr) + props, err = properties.LoadFromSlice( + requireCompileResponseJson(t, stdout).BuilderResult.GetBuildProperties()) + require.NoError(t, err) + require.True(t, props.ContainsKey("archive_file_path")) + require.NotContains(t, props.Get("archive_file_path"), "{build.path}") +} + +func requireCompileResponseJson(t *testing.T, stdout []byte) *cliCompileResponse { + var compileResponse cliCompileResponse + require.NoError(t, json.Unmarshal(stdout, &compileResponse)) + return &compileResponse } diff --git a/internal/integrationtest/core/core_test.go b/internal/integrationtest/core/core_test.go index 94e47b374f9..98117009724 100644 --- a/internal/integrationtest/core/core_test.go +++ b/internal/integrationtest/core/core_test.go @@ -992,3 +992,18 @@ func TestCoreInstallCreatesInstalledJson(t *testing.T) { sortedExpected := requirejson.Parse(t, expectedInstalledJson).Query("walk(if type == \"array\" then sort else . end)").String() require.JSONEq(t, sortedExpected, sortedInstalled) } + +func TestCoreInstallRunsToolPostInstallScript(t *testing.T) { + env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t) + defer env.CleanUp() + + url := "http://drazzy.com/package_drazzy.com_index.json" + + _, _, err := cli.Run("core", "update-index", "--additional-urls", url) + require.NoError(t, err) + + // Checks that the post_install script is correctly skipped on the CI + stdout, _, err := cli.Run("core", "install", "ATTinyCore:avr", "--verbose", "--additional-urls", url) + require.NoError(t, err) + require.Contains(t, string(stdout), "Skipping tool configuration.") +}