1616package preprocessor
1717
1818import (
19+ "bufio"
20+ "bytes"
1921 "context"
2022 "fmt"
23+ "io"
24+ "strconv"
2125 "strings"
2226
27+ "github.com/arduino/arduino-cli/arduino/builder/cpp"
28+ "github.com/arduino/arduino-cli/arduino/builder/preprocessor/internal/ctags"
29+ "github.com/arduino/arduino-cli/arduino/sketch"
2330 "github.com/arduino/arduino-cli/executils"
2431 "github.com/arduino/arduino-cli/i18n"
2532 "github.com/arduino/go-paths-helper"
@@ -29,6 +36,145 @@ import (
2936
3037var tr = i18n .Tr
3138
39+ // DebugPreprocessor when set to true the CTags preprocessor will output debugging info to stdout
40+ // this is useful for unit-testing to provide more infos
41+ var DebugPreprocessor bool
42+
43+ // PreprocessSketchWithCtags performs preprocessing of the arduino sketch using CTags.
44+ func PreprocessSketchWithCtags (sketch * sketch.Sketch , buildPath * paths.Path , includes paths.PathList , lineOffset int , buildProperties * properties.Map , onlyUpdateCompilationDatabase bool ) ([]byte , []byte , error ) {
45+ // Create a temporary working directory
46+ tmpDir , err := paths .MkTempDir ("" , "" )
47+ if err != nil {
48+ return nil , nil , err
49+ }
50+ defer tmpDir .RemoveAll ()
51+ ctagsTarget := tmpDir .Join ("sketch_merged.cpp" )
52+
53+ normalOutput := & bytes.Buffer {}
54+ verboseOutput := & bytes.Buffer {}
55+
56+ // Run GCC preprocessor
57+ sourceFile := buildPath .Join ("sketch" , sketch .MainFile .Base ()+ ".cpp" )
58+ gccStdout , gccStderr , err := GCC (sourceFile , ctagsTarget , includes , buildProperties )
59+ verboseOutput .Write (gccStdout )
60+ verboseOutput .Write (gccStderr )
61+ normalOutput .Write (gccStderr )
62+ if err != nil {
63+ if ! onlyUpdateCompilationDatabase {
64+ return normalOutput .Bytes (), verboseOutput .Bytes (), errors .WithStack (err )
65+ }
66+
67+ // Do not bail out if we are generating the compile commands database
68+ normalOutput .WriteString (fmt .Sprintf ("%s: %s" ,
69+ tr ("An error occurred adding prototypes" ),
70+ tr ("the compilation database may be incomplete or inaccurate" )))
71+ if err := sourceFile .CopyTo (ctagsTarget ); err != nil {
72+ return normalOutput .Bytes (), verboseOutput .Bytes (), errors .WithStack (err )
73+ }
74+ }
75+
76+ if src , err := ctagsTarget .ReadFile (); err == nil {
77+ filteredSource := filterSketchSource (sketch , bytes .NewReader (src ), false )
78+ if err := ctagsTarget .WriteFile ([]byte (filteredSource )); err != nil {
79+ return normalOutput .Bytes (), verboseOutput .Bytes (), err
80+ }
81+ } else {
82+ return normalOutput .Bytes (), verboseOutput .Bytes (), err
83+ }
84+
85+ // Run CTags on gcc-preprocessed source
86+ ctagsOutput , ctagsStdErr , err := RunCTags (ctagsTarget , buildProperties )
87+ verboseOutput .Write (ctagsStdErr )
88+ if err != nil {
89+ return normalOutput .Bytes (), verboseOutput .Bytes (), err
90+ }
91+
92+ // Parse CTags output
93+ parser := & ctags.Parser {}
94+ prototypes , firstFunctionLine := parser .Parse (ctagsOutput , sketch .MainFile )
95+ if firstFunctionLine == - 1 {
96+ firstFunctionLine = 0
97+ }
98+
99+ // Add prototypes to the original sketch source
100+ var source string
101+ if sourceData , err := sourceFile .ReadFile (); err == nil {
102+ source = string (sourceData )
103+ } else {
104+ return normalOutput .Bytes (), verboseOutput .Bytes (), err
105+ }
106+ source = strings .Replace (source , "\r \n " , "\n " , - 1 )
107+ source = strings .Replace (source , "\r " , "\n " , - 1 )
108+ sourceRows := strings .Split (source , "\n " )
109+ if isFirstFunctionOutsideOfSource (firstFunctionLine , sourceRows ) {
110+ return normalOutput .Bytes (), verboseOutput .Bytes (), nil
111+ }
112+
113+ insertionLine := firstFunctionLine + lineOffset - 1
114+ firstFunctionChar := len (strings .Join (sourceRows [:insertionLine ], "\n " )) + 1
115+ prototypeSection := composePrototypeSection (firstFunctionLine , prototypes )
116+ preprocessedSource := source [:firstFunctionChar ] + prototypeSection + source [firstFunctionChar :]
117+
118+ if DebugPreprocessor {
119+ fmt .Println ("#PREPROCESSED SOURCE" )
120+ prototypesRows := strings .Split (prototypeSection , "\n " )
121+ prototypesRows = prototypesRows [:len (prototypesRows )- 1 ]
122+ for i := 0 ; i < len (sourceRows )+ len (prototypesRows ); i ++ {
123+ if i < insertionLine {
124+ fmt .Printf (" |%s\n " , sourceRows [i ])
125+ } else if i < insertionLine + len (prototypesRows ) {
126+ fmt .Printf ("PRO|%s\n " , prototypesRows [i - insertionLine ])
127+ } else {
128+ fmt .Printf (" |%s\n " , sourceRows [i - len (prototypesRows )])
129+ }
130+ }
131+ fmt .Println ("#END OF PREPROCESSED SOURCE" )
132+ }
133+
134+ // Write back arduino-preprocess output to the sourceFile
135+ err = sourceFile .WriteFile ([]byte (preprocessedSource ))
136+ return normalOutput .Bytes (), verboseOutput .Bytes (), err
137+ }
138+
139+ func composePrototypeSection (line int , prototypes []* ctags.Prototype ) string {
140+ if len (prototypes ) == 0 {
141+ return ""
142+ }
143+
144+ str := joinPrototypes (prototypes )
145+ str += "\n #line "
146+ str += strconv .Itoa (line )
147+ str += " " + cpp .QuoteString (prototypes [0 ].File )
148+ str += "\n "
149+
150+ return str
151+ }
152+
153+ func joinPrototypes (prototypes []* ctags.Prototype ) string {
154+ prototypesSlice := []string {}
155+ for _ , proto := range prototypes {
156+ if signatureContainsaDefaultArg (proto ) {
157+ continue
158+ }
159+ prototypesSlice = append (prototypesSlice , "#line " + strconv .Itoa (proto .Line )+ " " + cpp .QuoteString (proto .File ))
160+ prototypeParts := []string {}
161+ if proto .Modifiers != "" {
162+ prototypeParts = append (prototypeParts , proto .Modifiers )
163+ }
164+ prototypeParts = append (prototypeParts , proto .Prototype )
165+ prototypesSlice = append (prototypesSlice , strings .Join (prototypeParts , " " ))
166+ }
167+ return strings .Join (prototypesSlice , "\n " )
168+ }
169+
170+ func signatureContainsaDefaultArg (proto * ctags.Prototype ) bool {
171+ return strings .Contains (proto .Prototype , "=" )
172+ }
173+
174+ func isFirstFunctionOutsideOfSource (firstFunctionLine int , sourceRows []string ) bool {
175+ return firstFunctionLine > len (sourceRows )- 1
176+ }
177+
32178// RunCTags performs a run of ctags on the given source file. Returns the ctags output and the stderr contents.
33179func RunCTags (sourceFile * paths.Path , buildProperties * properties.Map ) ([]byte , []byte , error ) {
34180 ctagsBuildProperties := properties .NewMap ()
@@ -60,3 +206,29 @@ func RunCTags(sourceFile *paths.Path, buildProperties *properties.Map) ([]byte,
60206 stderr = append ([]byte (args ), stderr ... )
61207 return stdout , stderr , err
62208}
209+
210+ func filterSketchSource (sketch * sketch.Sketch , source io.Reader , removeLineMarkers bool ) string {
211+ fileNames := paths .NewPathList ()
212+ fileNames .Add (sketch .MainFile )
213+ fileNames .AddAll (sketch .OtherSketchFiles )
214+
215+ inSketch := false
216+ filtered := ""
217+
218+ scanner := bufio .NewScanner (source )
219+ for scanner .Scan () {
220+ line := scanner .Text ()
221+ if filename := cpp .ParseLineMarker (line ); filename != nil {
222+ inSketch = fileNames .Contains (filename )
223+ if inSketch && removeLineMarkers {
224+ continue
225+ }
226+ }
227+
228+ if inSketch {
229+ filtered += line + "\n "
230+ }
231+ }
232+
233+ return filtered
234+ }
0 commit comments