Skip to content

Commit e9dc0f0

Browse files
committed
Added gcc output parser
1 parent 37d30b3 commit e9dc0f0

File tree

11 files changed

+1033
-0
lines changed

11 files changed

+1033
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package diagnostics
17+
18+
import (
19+
"bytes"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/arduino/arduino-cli/executils"
24+
semver "go.bug.st/relaxed-semver"
25+
)
26+
27+
// DetectedCompiler represents a compiler detected from a given command line
28+
type DetectedCompiler struct {
29+
Name string
30+
Family string
31+
Version *semver.Version
32+
DetailedVersion []string
33+
}
34+
35+
// This function is overridden for mocking unit tests
36+
var runProcess = func(args ...string) []string {
37+
if cmd, err := executils.NewProcess(nil, args...); err == nil {
38+
out := &bytes.Buffer{}
39+
cmd.RedirectStdoutTo(out)
40+
cmd.Run()
41+
return splitLines(out.Bytes())
42+
}
43+
return nil
44+
}
45+
46+
// DetectCompilerFromCommandLine tries to detect a compiler from a given command line.
47+
// If probeCompiler is true, the compiler may be executed with different flags to
48+
// infer the version or capabilities.
49+
func DetectCompilerFromCommandLine(args []string, probeCompiler bool) *DetectedCompiler {
50+
if len(args) == 0 {
51+
return nil
52+
}
53+
basename := filepath.Base(args[0])
54+
family := ""
55+
if strings.Contains(basename, "g++") || strings.Contains(basename, "gcc") {
56+
family = "gcc"
57+
}
58+
res := &DetectedCompiler{
59+
Name: basename,
60+
Family: family,
61+
}
62+
63+
if family == "gcc" && probeCompiler {
64+
// Run "gcc --version" to obtain more info
65+
res.DetailedVersion = runProcess(args[0], "--version")
66+
67+
// Usually on the first line we get the compiler architecture and
68+
// version (as last field), followed by the compiler license, for
69+
// example:
70+
//
71+
// g++ (Ubuntu 12.2.0-3ubuntu1) 12.2.0
72+
// Copyright (C) 2022 Free Software Foundation, Inc.
73+
// This is free software; see the source for copying conditions. There is NO
74+
// warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
75+
//
76+
if len(res.DetailedVersion) > 0 {
77+
split := strings.Split(res.DetailedVersion[0], " ")
78+
if len(split) >= 3 {
79+
res.Name = split[0]
80+
res.Version, _ = semver.Parse(split[len(split)-1])
81+
}
82+
}
83+
}
84+
return res
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package diagnostics
17+
18+
import (
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func init() {
26+
runProcess = mockedRunProcessToGetCompilerVersion
27+
}
28+
29+
func mockedRunProcessToGetCompilerVersion(args ...string) []string {
30+
if strings.HasSuffix(args[0], "7.3.0-atmel3.6.1-arduino7/bin/avr-g++") && args[1] == "--version" {
31+
return []string{
32+
"avr-g++ (GCC) 7.3.0",
33+
"Copyright (C) 2017 Free Software Foundation, Inc.",
34+
"This is free software; see the source for copying conditions. There is NO",
35+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
36+
"",
37+
}
38+
}
39+
if strings.HasSuffix(args[0], "7.3.0-atmel3.6.1-arduino7/bin/avr-gcc") && args[1] == "--version" {
40+
return []string{
41+
"avr-gcc (GCC) 7.3.0",
42+
"Copyright (C) 2017 Free Software Foundation, Inc.",
43+
"This is free software; see the source for copying conditions. There is NO",
44+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
45+
"",
46+
}
47+
}
48+
if strings.HasSuffix(args[0], "xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/bin/xtensa-esp32-elf-g++") && args[1] == "--version" {
49+
return []string{
50+
"xtensa-esp32-elf-g++ (crosstool-NG esp-2021r2-patch3) 8.4.0",
51+
"Copyright (C) 2018 Free Software Foundation, Inc.",
52+
"This is free software; see the source for copying conditions. There is NO",
53+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
54+
"",
55+
}
56+
}
57+
58+
panic("missing mock for command line: " + strings.Join(args, " "))
59+
}
60+
61+
func TestCompilerDetection(t *testing.T) {
62+
comp := DetectCompilerFromCommandLine([]string{"~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++"}, true)
63+
require.NotNil(t, comp)
64+
require.Equal(t, "gcc", comp.Family)
65+
require.Equal(t, "avr-g++", comp.Name)
66+
require.Equal(t, "7.3.0", comp.Version.String())
67+
68+
comp = DetectCompilerFromCommandLine([]string{"~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-gcc"}, true)
69+
require.NotNil(t, comp)
70+
require.Equal(t, "gcc", comp.Family)
71+
require.Equal(t, "avr-gcc", comp.Name)
72+
require.Equal(t, "7.3.0", comp.Version.String())
73+
74+
comp = DetectCompilerFromCommandLine([]string{"/home/megabug/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/bin/xtensa-esp32-elf-g++"}, true)
75+
require.NotNil(t, comp)
76+
require.Equal(t, "gcc", comp.Family)
77+
require.Equal(t, "xtensa-esp32-elf-g++", comp.Name)
78+
require.Equal(t, "8.4.0", comp.Version.String())
79+
}

Diff for: arduino/builder/internal/diagnostics/parser.go

+153
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,159 @@
1515

1616
package diagnostics
1717

18+
import (
19+
"fmt"
20+
"strings"
21+
22+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
23+
)
24+
1825
// CompilerOutputParserCB is a callback function that is called to feed a parser
1926
// with the plain-text compiler output.
2027
type CompilerOutputParserCB func(cmdline []string, out []byte)
28+
29+
// Diagnostics represents a list of diagnostics
30+
type Diagnostics []*Diagnostic
31+
32+
// Diagnostic represents a diagnostic (a compiler error, warning, note, etc.)
33+
type Diagnostic struct {
34+
Severity Severity `json:"severity,omitempty"`
35+
Message string `json:"message"`
36+
File string `json:"file,omitempty"`
37+
Line int `json:"line,omitempty"`
38+
Column int `json:"col,omitempty"`
39+
Context FullContext `json:"context,omitempty"`
40+
Suggestions Notes `json:"suggestions,omitempty"`
41+
}
42+
43+
// Severity is a diagnostic severity
44+
type Severity string
45+
46+
const (
47+
// SeverityUnspecified is the undefined severity
48+
SeverityUnspecified Severity = ""
49+
// SeverityWarning is a warning
50+
SeverityWarning = "WARNING"
51+
// SeverityError is an error
52+
SeverityError = "ERROR"
53+
// SeverityFatal is a fatal error
54+
SeverityFatal = "FATAL"
55+
)
56+
57+
// Notes represents a list of Note
58+
type Notes []*Note
59+
60+
// Note represents a compiler annotation or suggestion
61+
type Note struct {
62+
Message string `json:"message"`
63+
File string `json:"file,omitempty"`
64+
Line int `json:"line,omitempty"`
65+
Column int `json:"col,omitempty"`
66+
}
67+
68+
// FullContext represents a list of Context
69+
type FullContext []*Context
70+
71+
// Context represents a context, i.e. a reference to a file, line and column
72+
// or a part of the code that a Diagnostic refers to.
73+
type Context struct {
74+
Message string `json:"message"`
75+
File string `json:"file,omitempty"`
76+
Line int `json:"line,omitempty"`
77+
Column int `json:"col,omitempty"`
78+
}
79+
80+
// ParseCompilerOutput parses the output of a compiler and returns a list of
81+
// diagnostics.
82+
func ParseCompilerOutput(compiler *DetectedCompiler, out []byte) ([]*Diagnostic, error) {
83+
lines := splitLines(out)
84+
switch compiler.Family {
85+
case "gcc":
86+
return parseGccOutput(lines)
87+
default:
88+
return nil, fmt.Errorf("unsupported compiler: %s", compiler)
89+
}
90+
}
91+
92+
func splitLines(in []byte) []string {
93+
res := strings.Split(string(in), "\n")
94+
for i, line := range res {
95+
res[i] = strings.TrimSuffix(line, "\r")
96+
}
97+
if l := len(res) - 1; res[l] == "" {
98+
res = res[:l]
99+
}
100+
return res
101+
}
102+
103+
// ToRPC converts a Diagnostics to a slice of rpc.CompileDiagnostic
104+
func (d Diagnostics) ToRPC() []*rpc.CompileDiagnostic {
105+
if len(d) == 0 {
106+
return nil
107+
}
108+
var res []*rpc.CompileDiagnostic
109+
for _, diag := range d {
110+
res = append(res, diag.ToRPC())
111+
}
112+
return res
113+
}
114+
115+
// ToRPC converts a Diagnostic to a rpc.CompileDiagnostic
116+
func (d *Diagnostic) ToRPC() *rpc.CompileDiagnostic {
117+
if d == nil {
118+
return nil
119+
}
120+
return &rpc.CompileDiagnostic{
121+
Severity: string(d.Severity),
122+
Message: d.Message,
123+
File: d.File,
124+
Line: int64(d.Line),
125+
Column: int64(d.Column),
126+
Context: d.Context.ToRPC(),
127+
Notes: d.Suggestions.ToRPC(),
128+
}
129+
}
130+
131+
// ToRPC converts a Notes to a slice of rpc.CompileDiagnosticNote
132+
func (s Notes) ToRPC() []*rpc.CompileDiagnosticNote {
133+
var res []*rpc.CompileDiagnosticNote
134+
for _, suggestion := range s {
135+
res = append(res, suggestion.ToRPC())
136+
}
137+
return res
138+
}
139+
140+
// ToRPC converts a Note to a rpc.CompileDiagnosticNote
141+
func (s *Note) ToRPC() *rpc.CompileDiagnosticNote {
142+
if s == nil {
143+
return nil
144+
}
145+
return &rpc.CompileDiagnosticNote{
146+
File: s.File,
147+
Line: int64(s.Line),
148+
Column: int64(s.Column),
149+
Message: s.Message,
150+
}
151+
}
152+
153+
// ToRPC converts a FullContext to a slice of rpc.CompileDiagnosticContext
154+
func (t FullContext) ToRPC() []*rpc.CompileDiagnosticContext {
155+
var res []*rpc.CompileDiagnosticContext
156+
for _, trace := range t {
157+
res = append(res, trace.ToRPC())
158+
}
159+
return res
160+
}
161+
162+
// ToRPC converts a Context to a rpc.CompileDiagnosticContext
163+
func (d *Context) ToRPC() *rpc.CompileDiagnosticContext {
164+
if d == nil {
165+
return nil
166+
}
167+
return &rpc.CompileDiagnosticContext{
168+
File: d.File,
169+
Line: int64(d.Line),
170+
Column: int64(d.Column),
171+
Message: d.Message,
172+
}
173+
}

0 commit comments

Comments
 (0)