Skip to content

Added debug check command to check if a combination of board/programmer supports debugging. #2443

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

Merged
merged 8 commits into from
Dec 1, 2023
Merged
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
10 changes: 10 additions & 0 deletions arduino/cores/fqbn.go
Original file line number Diff line number Diff line change
@@ -30,6 +30,16 @@ type FQBN struct {
Configs *properties.Map
}

// MustParseFQBN extract an FQBN object from the input string
// or panics if the input is not a valid FQBN.
func MustParseFQBN(fqbnIn string) *FQBN {
res, err := ParseFQBN(fqbnIn)
if err != nil {
panic(err)
}
return res
}

// ParseFQBN extract an FQBN object from the input string
func ParseFQBN(fqbnIn string) (*FQBN, error) {
// Split fqbn
6 changes: 6 additions & 0 deletions commands/daemon/debug.go
Original file line number Diff line number Diff line change
@@ -66,3 +66,9 @@ func (s *ArduinoCoreServerImpl) GetDebugConfig(ctx context.Context, req *rpc.Get
res, err := cmd.GetDebugConfig(ctx, req)
return res, convertErrorToRPCStatus(err)
}

// IsDebugSupported checks if debugging is supported for a given configuration
func (s *ArduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.IsDebugSupportedRequest) (*rpc.IsDebugSupportedResponse, error) {
res, err := cmd.IsDebugSupported(ctx, req)
return res, convertErrorToRPCStatus(err)
}
2 changes: 1 addition & 1 deletion commands/debug/debug.go
Original file line number Diff line number Diff line change
@@ -118,7 +118,7 @@ func Debug(ctx context.Context, req *rpc.GetDebugConfigRequest, inStream io.Read

// getCommandLine compose a debug command represented by a core recipe
func getCommandLine(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer) ([]string, error) {
debugInfo, err := getDebugProperties(req, pme)
debugInfo, err := getDebugProperties(req, pme, false)
if err != nil {
return nil, err
}
98 changes: 80 additions & 18 deletions commands/debug/debug_info.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@ package debug
import (
"context"
"encoding/json"
"errors"
"reflect"
"slices"
"strconv"
"strings"
@@ -41,25 +43,83 @@ func GetDebugConfig(ctx context.Context, req *rpc.GetDebugConfigRequest) (*rpc.G
return nil, &arduino.InvalidInstanceError{}
}
defer release()
return getDebugProperties(req, pme)
return getDebugProperties(req, pme, false)
}

func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer) (*rpc.GetDebugConfigResponse, error) {
// TODO: make a generic function to extract sketch from request
// and remove duplication in commands/compile.go
if req.GetSketchPath() == "" {
return nil, &arduino.MissingSketchPathError{}
// IsDebugSupported checks if the given board/programmer configuration supports debugging.
func IsDebugSupported(ctx context.Context, req *rpc.IsDebugSupportedRequest) (*rpc.IsDebugSupportedResponse, error) {
pme, release := instances.GetPackageManagerExplorer(req.GetInstance())
if pme == nil {
return nil, &arduino.InvalidInstanceError{}
}
defer release()
configRequest := &rpc.GetDebugConfigRequest{
Instance: req.GetInstance(),
Fqbn: req.GetFqbn(),
SketchPath: "",
Port: req.GetPort(),
Interpreter: req.GetInterpreter(),
ImportDir: "",
Programmer: req.GetProgrammer(),
}
expectedOutput, err := getDebugProperties(configRequest, pme, true)
var x *arduino.FailedDebugError
if errors.As(err, &x) {
return &rpc.IsDebugSupportedResponse{DebuggingSupported: false}, nil
}
sketchPath := paths.New(req.GetSketchPath())
sk, err := sketch.New(sketchPath)
if err != nil {
return nil, &arduino.CantOpenSketchError{Cause: err}
return nil, err
}

// Compute the minimum FQBN required to get the same debug configuration.
// (i.e. the FQBN cleaned up of the options that do not affect the debugger configuration)
minimumFQBN := cores.MustParseFQBN(req.GetFqbn())
for _, config := range minimumFQBN.Configs.Keys() {
checkFQBN := minimumFQBN.Clone()
checkFQBN.Configs.Remove(config)
configRequest.Fqbn = checkFQBN.String()
checkOutput, err := getDebugProperties(configRequest, pme, true)
if err == nil && reflect.DeepEqual(expectedOutput, checkOutput) {
minimumFQBN.Configs.Remove(config)
}
}
return &rpc.IsDebugSupportedResponse{
DebuggingSupported: true,
DebugFqbn: minimumFQBN.String(),
}, nil
}

func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer, skipSketchChecks bool) (*rpc.GetDebugConfigResponse, error) {
var (
sketchName string
sketchDefaultFQBN string
sketchDefaultBuildPath *paths.Path
)
if !skipSketchChecks {
// TODO: make a generic function to extract sketch from request
// and remove duplication in commands/compile.go
if req.GetSketchPath() == "" {
return nil, &arduino.MissingSketchPathError{}
}
sketchPath := paths.New(req.GetSketchPath())
sk, err := sketch.New(sketchPath)
if err != nil {
return nil, &arduino.CantOpenSketchError{Cause: err}
}
sketchName = sk.Name
sketchDefaultFQBN = sk.GetDefaultFQBN()
sketchDefaultBuildPath = sk.DefaultBuildPath()
} else {
// Use placeholder sketch data
sketchName = "Sketch"
sketchDefaultFQBN = ""
sketchDefaultBuildPath = paths.New("SketchBuildPath")
}

// XXX Remove this code duplication!!
fqbnIn := req.GetFqbn()
if fqbnIn == "" && sk != nil {
fqbnIn = sk.GetDefaultFQBN()
if fqbnIn == "" {
fqbnIn = sketchDefaultFQBN
}
if fqbnIn == "" {
return nil, &arduino.MissingFQBNError{}
@@ -109,16 +169,18 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl
if importDir := req.GetImportDir(); importDir != "" {
importPath = paths.New(importDir)
} else {
importPath = sk.DefaultBuildPath()
}
if !importPath.Exist() {
return nil, &arduino.NotFoundError{Message: tr("Compiled sketch not found in %s", importPath)}
importPath = sketchDefaultBuildPath
}
if !importPath.IsDir() {
return nil, &arduino.NotFoundError{Message: tr("Expected compiled sketch in directory %s, but is a file instead", importPath)}
if !skipSketchChecks {
if !importPath.Exist() {
return nil, &arduino.NotFoundError{Message: tr("Compiled sketch not found in %s", importPath)}
}
if !importPath.IsDir() {
return nil, &arduino.NotFoundError{Message: tr("Expected compiled sketch in directory %s, but is a file instead", importPath)}
}
}
toolProperties.SetPath("build.path", importPath)
toolProperties.Set("build.project_name", sk.Name+".ino")
toolProperties.Set("build.project_name", sketchName+".ino")

// Set debug port property
port := req.GetPort()
29 changes: 17 additions & 12 deletions internal/cli/debug/debug.go
Original file line number Diff line number Diff line change
@@ -36,27 +36,31 @@ import (
"github.com/spf13/cobra"
)

var (
fqbnArg arguments.Fqbn
portArgs arguments.Port
interpreter string
importDir string
printInfo bool
programmer arguments.Programmer
tr = i18n.Tr
)
var tr = i18n.Tr

// NewCommand created a new `upload` command
func NewCommand() *cobra.Command {
var (
fqbnArg arguments.Fqbn
portArgs arguments.Port
interpreter string
importDir string
printInfo bool
programmer arguments.Programmer
)

debugCommand := &cobra.Command{
Use: "debug",
Short: tr("Debug Arduino sketches."),
Long: tr("Debug Arduino sketches. (this command opens an interactive gdb session)"),
Example: " " + os.Args[0] + " debug -b arduino:samd:mkr1000 -P atmel_ice /home/user/Arduino/MySketch",
Args: cobra.MaximumNArgs(1),
Run: runDebugCommand,
Run: func(cmd *cobra.Command, args []string) {
runDebugCommand(args, &portArgs, &fqbnArg, interpreter, importDir, &programmer, printInfo)
},
}

debugCommand.AddCommand(newDebugCheckCommand())
fqbnArg.AddToCommand(debugCommand)
portArgs.AddToCommand(debugCommand)
programmer.AddToCommand(debugCommand)
@@ -67,7 +71,8 @@ func NewCommand() *cobra.Command {
return debugCommand
}

func runDebugCommand(command *cobra.Command, args []string) {
func runDebugCommand(args []string, portArgs *arguments.Port, fqbnArg *arguments.Fqbn,
interpreter string, importDir string, programmer *arguments.Programmer, printInfo bool) {
instance := instance.CreateAndInit()
logrus.Info("Executing `arduino-cli debug`")

@@ -81,7 +86,7 @@ func runDebugCommand(command *cobra.Command, args []string) {
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}
fqbn, port := arguments.CalculateFQBNAndPort(&portArgs, &fqbnArg, instance, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol())
fqbn, port := arguments.CalculateFQBNAndPort(portArgs, fqbnArg, instance, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol())
debugConfigRequested := &rpc.GetDebugConfigRequest{
Instance: instance,
Fqbn: fqbn,
89 changes: 89 additions & 0 deletions internal/cli/debug/debug_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// This file is part of arduino-cli.
//
// Copyright 2023 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 debug

import (
"context"
"os"

"github.com/arduino/arduino-cli/commands/debug"
"github.com/arduino/arduino-cli/internal/cli/arguments"
"github.com/arduino/arduino-cli/internal/cli/feedback"
"github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/instance"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func newDebugCheckCommand() *cobra.Command {
var (
fqbnArg arguments.Fqbn
portArgs arguments.Port
interpreter string
programmer arguments.Programmer
)
debugCheckCommand := &cobra.Command{
Use: "check",
Short: tr("Check if the given board/programmer combination supports debugging."),
Example: " " + os.Args[0] + " debug check -b arduino:samd:mkr1000 -P atmel_ice",
Run: func(cmd *cobra.Command, args []string) {
runDebugCheckCommand(&portArgs, &fqbnArg, interpreter, &programmer)
},
}
fqbnArg.AddToCommand(debugCheckCommand)
portArgs.AddToCommand(debugCheckCommand)
programmer.AddToCommand(debugCheckCommand)
debugCheckCommand.Flags().StringVar(&interpreter, "interpreter", "console", tr("Debug interpreter e.g.: %s", "console, mi, mi1, mi2, mi3"))
return debugCheckCommand
}

func runDebugCheckCommand(portArgs *arguments.Port, fqbnArg *arguments.Fqbn, interpreter string, programmerArg *arguments.Programmer) {
instance := instance.CreateAndInit()
logrus.Info("Executing `arduino-cli debug`")

port, err := portArgs.GetPort(instance, "", "")
if err != nil {
feedback.FatalError(err, feedback.ErrBadArgument)
}
fqbn := fqbnArg.String()
resp, err := debug.IsDebugSupported(context.Background(), &rpc.IsDebugSupportedRequest{
Instance: instance,
Fqbn: fqbn,
Port: port,
Interpreter: interpreter,
Programmer: programmerArg.String(instance, fqbn),
})
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}
feedback.PrintResult(&debugCheckResult{result.NewIsDebugSupportedResponse(resp)})
}

type debugCheckResult struct {
Result *result.IsDebugSupportedResponse
}

func (d *debugCheckResult) Data() interface{} {
return d.Result
}

func (d *debugCheckResult) String() string {
if d.Result.DebuggingSupported {
return tr("The given board/programmer configuration supports debugging.")
}
return tr("The given board/programmer configuration does NOT support debugging.")
}
12 changes: 12 additions & 0 deletions internal/cli/feedback/result/rpc.go
Original file line number Diff line number Diff line change
@@ -1057,3 +1057,15 @@ func NewCompileDiagnosticNote(cdn *rpc.CompileDiagnosticNote) *CompileDiagnostic
Column: cdn.GetColumn(),
}
}

type IsDebugSupportedResponse struct {
DebuggingSupported bool `json:"debugging_supported"`
DebugFQBN string `json:"debug_fqbn,omitempty"`
}

func NewIsDebugSupportedResponse(resp *rpc.IsDebugSupportedResponse) *IsDebugSupportedResponse {
return &IsDebugSupportedResponse{
DebuggingSupported: resp.GetDebuggingSupported(),
DebugFQBN: resp.GetDebugFqbn(),
}
}
4 changes: 4 additions & 0 deletions internal/cli/feedback/result/rpc_test.go
Original file line number Diff line number Diff line change
@@ -217,6 +217,10 @@ func TestAllFieldAreMapped(t *testing.T) {
compileDiagnosticNoteRpc := &rpc.CompileDiagnosticNote{}
compileDiagnosticNoteResult := result.NewCompileDiagnosticNote(compileDiagnosticNoteRpc)
mustContainsAllPropertyOfRpcStruct(t, compileDiagnosticNoteRpc, compileDiagnosticNoteResult)

isDebugSupportedResponseRpc := &rpc.IsDebugSupportedResponse{}
isDebugSupportedResponseResult := result.NewIsDebugSupportedResponse(isDebugSupportedResponseRpc)
mustContainsAllPropertyOfRpcStruct(t, isDebugSupportedResponseRpc, isDebugSupportedResponseResult)
}

func TestEnumsMapsEveryRpcCounterpart(t *testing.T) {
57 changes: 51 additions & 6 deletions internal/integrationtest/debug/debug_test.go
Original file line number Diff line number Diff line change
@@ -33,13 +33,22 @@ func TestDebug(t *testing.T) {
require.NoError(t, err)

// Install cores
_, _, err = cli.Run("core", "install", "arduino:avr")
require.NoError(t, err)
_, _, err = cli.Run("core", "install", "arduino:samd")
require.NoError(t, err)

// Install custom core
customHw, err := paths.New("testdata", "hardware").Abs()
require.NoError(t, err)
err = customHw.CopyDirTo(cli.SketchbookDir().Join("hardware"))
require.NoError(t, err)

integrationtest.CLISubtests{
{"Start", testDebuggerStarts},
{"WithPdeSketchStarts", testDebuggerWithPdeSketchStarts},
{"DebugInformation", testAllDebugInformation},
{"DebugCheck", testDebugCheck},
}.Run(t, env, cli)
}

@@ -99,12 +108,6 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
_, _, err := cli.Run("sketch", "new", sketchPath.String())
require.NoError(t, err)

// Install custom core
customHw, err := paths.New("testdata", "hardware").Abs()
require.NoError(t, err)
err = customHw.CopyDirTo(cli.SketchbookDir().Join("hardware"))
require.NoError(t, err)

// Build sketch
_, _, err = cli.Run("compile", "-b", "my:samd:my", sketchPath.String(), "--format", "json")
require.NoError(t, err)
@@ -331,3 +334,45 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
}
}
}

func testDebugCheck(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
_, _, err := cli.Run("debug", "check", "-b", "arduino:samd:mkr1000")
require.Error(t, err)

out, _, err := cli.Run("debug", "check", "-b", "arduino:samd:mkr1000", "-P", "atmel_ice")
require.NoError(t, err)
require.Contains(t, string(out), "The given board/programmer configuration supports debugging.")

out, _, err = cli.Run("debug", "check", "-b", "arduino:samd:mkr1000", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Query(t, out, `.debugging_supported`, `true`)

out, _, err = cli.Run("debug", "check", "-b", "arduino:avr:uno", "-P", "atmel_ice")
require.NoError(t, err)
require.Contains(t, string(out), "The given board/programmer configuration does NOT support debugging.")

out, _, err = cli.Run("debug", "check", "-b", "arduino:avr:uno", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Query(t, out, `.debugging_supported`, `false`)

// Test minimum FQBN compute
out, _, err = cli.Run("debug", "check", "-b", "my:samd:my5", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Contains(t, out, `{ "debugging_supported" : false }`)

out, _, err = cli.Run("debug", "check", "-b", "my:samd:my5:dbg=on", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Contains(t, out, `{ "debugging_supported" : true, "debug_fqbn" : "my:samd:my5:dbg=on" }`)

out, _, err = cli.Run("debug", "check", "-b", "my:samd:my5:dbg=on,cpu=150m", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Contains(t, out, `{ "debugging_supported" : true, "debug_fqbn" : "my:samd:my5:dbg=on" }`)

out, _, err = cli.Run("debug", "check", "-b", "my:samd:my6", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Contains(t, out, `{ "debugging_supported" : true, "debug_fqbn" : "my:samd:my6" }`)

out, _, err = cli.Run("debug", "check", "-b", "my:samd:my6:dbg=on", "-P", "atmel_ice", "--format", "json")
require.NoError(t, err)
requirejson.Contains(t, out, `{ "debugging_supported" : true, "debug_fqbn" : "my:samd:my6" }`)
}
Original file line number Diff line number Diff line change
@@ -112,3 +112,39 @@ my4.build.mcu=test2
# this one will be overwritten by additional_config
my4.debug.svd_file=svd-file
my4.debug.additional_config=build.debug.config.{build.mcu}

# menu options for the following boards
menu.dbg=Debugger
menu.cpu=CPU Speed

# This does not support debug by default but only after selecting a menu option
my5.name=5th Board
my5.build.core=arduino:arduino
my5.build.variant=arduino:mkr1000
my5.debug.toolchain.path=gcc-path
my5.debug.toolchain.prefix=gcc-prefix
my5.debug.server.openocd.path=openocd-path
my5.debug.server.openocd.scripts_dir=openocd-scripts-dir
my5.debug.server.openocd.script=single-script
my5.debug.executable=
my5.menu.dbg.off=Debug Disabled
my5.menu.dbg.on=Debug Enabled
my5.menu.dbg.on.debug.executable=YES
my5.menu.cpu.100m=100Mhz
my5.menu.cpu.150m=150Mhz

# this one has debugger enabled by default
my6.name=5th Board
my6.build.core=arduino:arduino
my6.build.variant=arduino:mkr1000
my6.debug.toolchain.path=gcc-path
my6.debug.toolchain.prefix=gcc-prefix
my6.debug.server.openocd.path=openocd-path
my6.debug.server.openocd.scripts_dir=openocd-scripts-dir
my6.debug.server.openocd.script=single-script
my6.debug.executable=
my6.menu.dbg.on=Debug Enabled
my6.menu.dbg.on.debug.executable=YES
my6.menu.dbg.off=Debug Disabled
my6.menu.cpu.100m=100Mhz
my6.menu.cpu.150m=150Mhz
320 changes: 166 additions & 154 deletions rpc/cc/arduino/cli/commands/v1/commands.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/commands.proto
Original file line number Diff line number Diff line change
@@ -182,6 +182,11 @@ service ArduinoCoreService {
// Start a debug session and communicate with the debugger tool.
rpc Debug(stream DebugRequest) returns (stream DebugResponse) {}

// Determine if debugging is suported given a specific configuration.
rpc IsDebugSupported(IsDebugSupportedRequest)
returns (IsDebugSupportedResponse) {}

// Query the debugger information given a specific configuration.
rpc GetDebugConfig(GetDebugConfigRequest) returns (GetDebugConfigResponse) {}

// List all the settings.
41 changes: 41 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/commands_grpc.pb.go
534 changes: 365 additions & 169 deletions rpc/cc/arduino/cli/commands/v1/debug.pb.go

Large diffs are not rendered by default.

41 changes: 33 additions & 8 deletions rpc/cc/arduino/cli/commands/v1/debug.proto
Original file line number Diff line number Diff line change
@@ -43,6 +43,39 @@ message DebugRequest {
bool send_interrupt = 3;
}

// The streaming response may contain chunks of data from the debugger or an
// error.
message DebugResponse {
// Incoming data from the debugger tool.
bytes data = 1;
// Incoming error output from the debugger tool.
string error = 2;
}

message IsDebugSupportedRequest {
// Arduino Core Service instance from the `Init` response.
Instance instance = 1;
// Fully qualified board name of the board in use (e.g.,
// `arduino:samd:mkr1000`).
string fqbn = 2;
// Port of the debugger (optional).
Port port = 3;
// Which GDB command interpreter to use.
string interpreter = 4;
// The programmer to use for debugging.
string programmer = 5;
}

message IsDebugSupportedResponse {
// True if debugging is supported
bool debugging_supported = 1;
// This is the same FQBN given in the IsDebugSupportedRequest but cleaned
// up of the board options that do not affect the debugger configuration.
// It may be used by clients/IDE to group slightly different boards option
// selections under the same debug configuration.
string debug_fqbn = 2;
}

message GetDebugConfigRequest {
// Arduino Core Service instance from the `Init` response.
Instance instance = 1;
@@ -65,14 +98,6 @@ message GetDebugConfigRequest {
string programmer = 9;
}

//
message DebugResponse {
// Incoming data from the debugger tool.
bytes data = 1;
// Incoming error output from the debugger tool.
string error = 2;
}

message GetDebugConfigResponse {
// The executable binary to debug
string executable = 1;