Skip to content

Commit afc0826

Browse files
committed
Implemented proper .d file parser
1 parent 3eecf20 commit afc0826

File tree

14 files changed

+1493
-78
lines changed

14 files changed

+1493
-78
lines changed

internal/arduino/builder/internal/utils/ansi_others.go renamed to internal/arduino/builder/cpp/ansi_others.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
//go:build !windows
1717

18-
package utils
18+
package cpp
1919

2020
import (
2121
"errors"

internal/arduino/builder/internal/utils/ansi_windows.go renamed to internal/arduino/builder/cpp/ansi_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Arduino software without disclosing the source code of your own applications.
1414
// To purchase a commercial license, send an email to [email protected].
1515

16-
package utils
16+
package cpp
1717

1818
import (
1919
"golang.org/x/sys/windows"
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2025 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 cpp
17+
18+
import (
19+
"errors"
20+
"runtime"
21+
"strings"
22+
"unicode"
23+
24+
"github.com/arduino/go-paths-helper"
25+
"go.bug.st/f"
26+
)
27+
28+
// Dependencies represents the dependencies of a source file.
29+
type Dependencies struct {
30+
ObjectFile string
31+
Dependencies []string
32+
}
33+
34+
// ReadDepFile reads a dependency file and returns the dependencies.
35+
// It may return nil if the dependency file is empty.
36+
func ReadDepFile(depFilePath *paths.Path) (*Dependencies, error) {
37+
depFileData, err := depFilePath.ReadFile()
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
if runtime.GOOS == "windows" {
43+
// This is required because on Windows we don't know which encoding is used
44+
// by gcc to write the dep file (it could be UTF-8 or any of the Windows
45+
// ANSI mappings).
46+
if decoded, err := convertAnsiBytesToString(depFileData); err == nil {
47+
if res, err := readDepFile(decoded); err == nil && res != nil {
48+
return res, nil
49+
}
50+
}
51+
// Fallback to UTF-8...
52+
}
53+
54+
return readDepFile(string(depFileData))
55+
}
56+
57+
func readDepFile(depFile string) (*Dependencies, error) {
58+
rows, err := unescapeAndSplit(strings.ReplaceAll(depFile, "\r\n", "\n"))
59+
if err != nil {
60+
return nil, err
61+
}
62+
rows = f.Map(rows, strings.TrimSpace)
63+
rows = f.Filter(rows, f.NotEquals(""))
64+
if len(rows) == 0 {
65+
return &Dependencies{}, nil
66+
}
67+
68+
// The first line of the depfile contains the path to the object file to generate.
69+
// The second line of the depfile contains the path to the source file.
70+
// All subsequent lines contain the header files necessary to compile the object file.
71+
72+
if !strings.HasSuffix(rows[0], ":") {
73+
return nil, errors.New("no colon in first item of depfile")
74+
}
75+
res := &Dependencies{
76+
ObjectFile: strings.TrimSuffix(rows[0], ":"),
77+
Dependencies: rows[1:],
78+
}
79+
return res, nil
80+
}
81+
82+
func unescapeAndSplit(s string) ([]string, error) {
83+
var res []string
84+
backslash := false
85+
dollar := false
86+
current := strings.Builder{}
87+
for _, c := range s {
88+
if backslash {
89+
switch c {
90+
case ' ':
91+
current.WriteByte(' ')
92+
case 't':
93+
current.WriteByte('\t')
94+
case '#':
95+
current.WriteByte('#')
96+
case '\\':
97+
current.WriteByte('\\')
98+
case '\n':
99+
// ignore
100+
default:
101+
return nil, errors.New("invalid escape sequence: \\" + string(c))
102+
}
103+
backslash = false
104+
continue
105+
}
106+
if dollar {
107+
if c != '$' {
108+
return nil, errors.New("invalid dollar sequence: $" + string(c))
109+
}
110+
current.WriteByte('$')
111+
dollar = false
112+
continue
113+
}
114+
115+
if c == '\\' {
116+
backslash = true
117+
continue
118+
}
119+
if c == '$' {
120+
dollar = true
121+
continue
122+
}
123+
124+
if unicode.IsSpace(c) {
125+
if current.Len() > 0 {
126+
res = append(res, current.String())
127+
current.Reset()
128+
}
129+
continue
130+
}
131+
current.WriteRune(c)
132+
}
133+
if backslash || dollar {
134+
return nil, errors.New("unclosed escape sequence at end of depfile")
135+
}
136+
if current.Len() > 0 {
137+
res = append(res, current.String())
138+
}
139+
return res, nil
140+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2025 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 cpp
17+
18+
import (
19+
"testing"
20+
21+
"github.com/arduino/go-paths-helper"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestDepFileReader(t *testing.T) {
26+
t.Run("0", func(t *testing.T) {
27+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.0.d"))
28+
require.NoError(t, err)
29+
require.NotNil(t, deps)
30+
require.Len(t, deps.Dependencies, 302)
31+
require.Equal(t, "sketch.ino.cpp.o", deps.ObjectFile)
32+
require.Equal(t, "/home/megabug/Arduino/sketch/build/sketch/sketch.ino.cpp.merged", deps.Dependencies[0])
33+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/generated/zephyr/autoconf.h", deps.Dependencies[1])
34+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/zephyr/toolchain/zephyr_stdint.h", deps.Dependencies[2])
35+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/libraries/Arduino_RPCLite/src/dispatcher.h", deps.Dependencies[301])
36+
})
37+
t.Run("1", func(t *testing.T) {
38+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.1.d"))
39+
require.NoError(t, err)
40+
require.NotNil(t, deps)
41+
require.Equal(t, "sketch.ino.o", deps.ObjectFile)
42+
require.Len(t, deps.Dependencies, 302)
43+
require.Equal(t, "/home/megabug/Arduino/sketch/build/sketch/sketch.ino.cpp", deps.Dependencies[0])
44+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/generated/zephyr/autoconf.h", deps.Dependencies[1])
45+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/zephyr/toolchain/zephyr_stdint.h", deps.Dependencies[2])
46+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/libraries/Arduino_RPCLite/src/dispatcher.h", deps.Dependencies[301])
47+
})
48+
t.Run("2", func(t *testing.T) {
49+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.2.d"))
50+
require.NoError(t, err)
51+
require.NotNil(t, deps)
52+
require.Equal(t, "ske tch.ino.cpp.o", deps.ObjectFile)
53+
require.Len(t, deps.Dependencies, 302)
54+
require.Equal(t, "/home/megabug/Arduino/ske tch/build/sketch/ske tch.ino.cpp.merged", deps.Dependencies[0])
55+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/generated/zephyr/autoconf.h", deps.Dependencies[1])
56+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/variants/b_u585i_iot02a_stm32u585xx/llext-edk/include/zephyr/include/zephyr/toolchain/zephyr_stdint.h", deps.Dependencies[2])
57+
require.Equal(t, "/home/megabug/.arduino15/packages/arduino/hardware/zephyr/0.10.0-rc.10/libraries/Arduino_RPCLite/src/dispatcher.h", deps.Dependencies[301])
58+
})
59+
t.Run("3", func(t *testing.T) {
60+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.3.d"))
61+
require.NoError(t, err)
62+
require.NotNil(t, deps)
63+
require.Equal(t, "myfile.o", deps.ObjectFile)
64+
require.Len(t, deps.Dependencies, 3)
65+
require.Equal(t, "/some/path\twith/tabs and spaces/file.cpp", deps.Dependencies[0])
66+
require.Equal(t, "/some/other$/path#/file.h", deps.Dependencies[1])
67+
require.Equal(t, "/yet/ano\\ther/path/file.h", deps.Dependencies[2])
68+
})
69+
t.Run("4", func(t *testing.T) {
70+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.4.d"))
71+
require.EqualError(t, err, "invalid dollar sequence: $a")
72+
require.Nil(t, deps)
73+
})
74+
t.Run("5", func(t *testing.T) {
75+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.5.d"))
76+
require.EqualError(t, err, "invalid escape sequence: \\h")
77+
require.Nil(t, deps)
78+
})
79+
t.Run("6", func(t *testing.T) {
80+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.6.d"))
81+
require.EqualError(t, err, "unclosed escape sequence at end of depfile")
82+
require.Nil(t, deps)
83+
})
84+
t.Run("7", func(t *testing.T) {
85+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.7.d"))
86+
require.EqualError(t, err, "no colon in first item of depfile")
87+
require.Nil(t, deps)
88+
})
89+
t.Run("8", func(t *testing.T) {
90+
deps, err := ReadDepFile(paths.New("testdata", "depcheck.8.d"))
91+
require.NoError(t, err)
92+
require.Nil(t, deps.Dependencies)
93+
require.Empty(t, deps.ObjectFile)
94+
})
95+
t.Run("9", func(t *testing.T) {
96+
deps, err := ReadDepFile(paths.New("testdata", "nonexistent.d"))
97+
require.Error(t, err)
98+
require.Nil(t, deps)
99+
})
100+
}

0 commit comments

Comments
 (0)