• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 Google Inc. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package blueprint
16 
17 import (
18 	"io"
19 	"strings"
20 	"unicode"
21 )
22 
23 const (
24 	indentWidth    = 4
25 	maxIndentDepth = 2
26 	lineWidth      = 80
27 )
28 
29 var indentString = strings.Repeat(" ", indentWidth*maxIndentDepth)
30 
31 type StringWriterWriter interface {
32 	io.StringWriter
33 	io.Writer
34 }
35 
36 type ninjaWriter struct {
37 	writer io.StringWriter
38 
39 	justDidBlankLine bool // true if the last operation was a BlankLine
40 }
41 
42 func newNinjaWriter(writer io.StringWriter) *ninjaWriter {
43 	return &ninjaWriter{
44 		writer: writer,
45 	}
46 }
47 
48 func (n *ninjaWriter) Comment(comment string) error {
49 	n.justDidBlankLine = false
50 
51 	const lineHeaderLen = len("# ")
52 	const maxLineLen = lineWidth - lineHeaderLen
53 
54 	var lineStart, lastSplitPoint int
55 	for i, r := range comment {
56 		if unicode.IsSpace(r) {
57 			// We know we can safely split the line here.
58 			lastSplitPoint = i + 1
59 		}
60 
61 		var line string
62 		var writeLine bool
63 		switch {
64 		case r == '\n':
65 			// Output the line without trimming the left so as to allow comments
66 			// to contain their own indentation.
67 			line = strings.TrimRightFunc(comment[lineStart:i], unicode.IsSpace)
68 			writeLine = true
69 
70 		case (i-lineStart > maxLineLen) && (lastSplitPoint > lineStart):
71 			// The line has grown too long and is splittable.  Split it at the
72 			// last split point.
73 			line = strings.TrimSpace(comment[lineStart:lastSplitPoint])
74 			writeLine = true
75 		}
76 
77 		if writeLine {
78 			line = strings.TrimSpace("# "+line) + "\n"
79 			_, err := n.writer.WriteString(line)
80 			if err != nil {
81 				return err
82 			}
83 			lineStart = lastSplitPoint
84 		}
85 	}
86 
87 	if lineStart != len(comment) {
88 		line := strings.TrimSpace(comment[lineStart:])
89 		_, err := n.writer.WriteString("# ")
90 		if err != nil {
91 			return err
92 		}
93 		_, err = n.writer.WriteString(line)
94 		if err != nil {
95 			return err
96 		}
97 		_, err = n.writer.WriteString("\n")
98 		if err != nil {
99 			return err
100 		}
101 	}
102 
103 	return nil
104 }
105 
106 func (n *ninjaWriter) Pool(name string) error {
107 	n.justDidBlankLine = false
108 	return n.writeStatement("pool", name)
109 }
110 
111 func (n *ninjaWriter) Rule(name string) error {
112 	n.justDidBlankLine = false
113 	return n.writeStatement("rule", name)
114 }
115 
116 func (n *ninjaWriter) Build(comment string, rule string, outputs, implicitOuts,
117 	explicitDeps, implicitDeps, orderOnlyDeps, validations []ninjaString,
118 	pkgNames map[*packageContext]string) error {
119 
120 	n.justDidBlankLine = false
121 
122 	const lineWrapLen = len(" $")
123 	const maxLineLen = lineWidth - lineWrapLen
124 
125 	wrapper := &ninjaWriterWithWrap{
126 		ninjaWriter: n,
127 		maxLineLen:  maxLineLen,
128 	}
129 
130 	if comment != "" {
131 		err := wrapper.Comment(comment)
132 		if err != nil {
133 			return err
134 		}
135 	}
136 
137 	wrapper.WriteString("build")
138 
139 	for _, output := range outputs {
140 		wrapper.Space()
141 		output.ValueWithEscaper(wrapper, pkgNames, outputEscaper)
142 	}
143 
144 	if len(implicitOuts) > 0 {
145 		wrapper.WriteStringWithSpace("|")
146 
147 		for _, out := range implicitOuts {
148 			wrapper.Space()
149 			out.ValueWithEscaper(wrapper, pkgNames, outputEscaper)
150 		}
151 	}
152 
153 	wrapper.WriteString(":")
154 
155 	wrapper.WriteStringWithSpace(rule)
156 
157 	for _, dep := range explicitDeps {
158 		wrapper.Space()
159 		dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper)
160 	}
161 
162 	if len(implicitDeps) > 0 {
163 		wrapper.WriteStringWithSpace("|")
164 
165 		for _, dep := range implicitDeps {
166 			wrapper.Space()
167 			dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper)
168 		}
169 	}
170 
171 	if len(orderOnlyDeps) > 0 {
172 		wrapper.WriteStringWithSpace("||")
173 
174 		for _, dep := range orderOnlyDeps {
175 			wrapper.Space()
176 			dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper)
177 		}
178 	}
179 
180 	if len(validations) > 0 {
181 		wrapper.WriteStringWithSpace("|@")
182 
183 		for _, dep := range validations {
184 			wrapper.Space()
185 			dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper)
186 		}
187 	}
188 
189 	return wrapper.Flush()
190 }
191 
192 func (n *ninjaWriter) Assign(name, value string) error {
193 	n.justDidBlankLine = false
194 	_, err := n.writer.WriteString(name)
195 	if err != nil {
196 		return err
197 	}
198 	_, err = n.writer.WriteString(" = ")
199 	if err != nil {
200 		return err
201 	}
202 	_, err = n.writer.WriteString(value)
203 	if err != nil {
204 		return err
205 	}
206 	_, err = n.writer.WriteString("\n")
207 	if err != nil {
208 		return err
209 	}
210 	return nil
211 }
212 
213 func (n *ninjaWriter) ScopedAssign(name, value string) error {
214 	n.justDidBlankLine = false
215 	_, err := n.writer.WriteString(indentString[:indentWidth])
216 	if err != nil {
217 		return err
218 	}
219 	_, err = n.writer.WriteString(name)
220 	if err != nil {
221 		return err
222 	}
223 	_, err = n.writer.WriteString(" = ")
224 	if err != nil {
225 		return err
226 	}
227 	_, err = n.writer.WriteString(value)
228 	if err != nil {
229 		return err
230 	}
231 	_, err = n.writer.WriteString("\n")
232 	if err != nil {
233 		return err
234 	}
235 	return nil
236 }
237 
238 func (n *ninjaWriter) Default(pkgNames map[*packageContext]string, targets ...ninjaString) error {
239 	n.justDidBlankLine = false
240 
241 	const lineWrapLen = len(" $")
242 	const maxLineLen = lineWidth - lineWrapLen
243 
244 	wrapper := &ninjaWriterWithWrap{
245 		ninjaWriter: n,
246 		maxLineLen:  maxLineLen,
247 	}
248 
249 	wrapper.WriteString("default")
250 
251 	for _, target := range targets {
252 		wrapper.Space()
253 		target.ValueWithEscaper(wrapper, pkgNames, outputEscaper)
254 	}
255 
256 	return wrapper.Flush()
257 }
258 
259 func (n *ninjaWriter) Subninja(file string) error {
260 	n.justDidBlankLine = false
261 	return n.writeStatement("subninja", file)
262 }
263 
264 func (n *ninjaWriter) BlankLine() (err error) {
265 	// We don't output multiple blank lines in a row.
266 	if !n.justDidBlankLine {
267 		n.justDidBlankLine = true
268 		_, err = n.writer.WriteString("\n")
269 	}
270 	return err
271 }
272 
273 func (n *ninjaWriter) writeStatement(directive, name string) error {
274 	_, err := n.writer.WriteString(directive + " ")
275 	if err != nil {
276 		return err
277 	}
278 	_, err = n.writer.WriteString(name)
279 	if err != nil {
280 		return err
281 	}
282 	_, err = n.writer.WriteString("\n")
283 	if err != nil {
284 		return err
285 	}
286 	return nil
287 }
288 
289 // ninjaWriterWithWrap is an io.StringWriter that writes through to a ninjaWriter, but supports
290 // user-readable line wrapping on boundaries when ninjaWriterWithWrap.Space is called.
291 // It collects incoming calls to WriteString until either the line length is exceeded, in which case
292 // it inserts a wrap before the pending strings and then writes them, or the next call to Space, in
293 // which case it writes out the pending strings.
294 //
295 // WriteString never returns an error, all errors are held until Flush is called.  Once an error has
296 // occurred all writes become noops.
297 type ninjaWriterWithWrap struct {
298 	*ninjaWriter
299 	// pending lists the strings that have been written since the last call to Space.
300 	pending []string
301 
302 	// pendingLen accumulates the lengths of the strings in pending.
303 	pendingLen int
304 
305 	// lineLen accumulates the number of bytes on the current line.
306 	lineLen int
307 
308 	// maxLineLen is the length of the line before wrapping.
309 	maxLineLen int
310 
311 	// space is true if the strings in pending should be preceded by a space.
312 	space bool
313 
314 	// err holds any error that has occurred to return in Flush.
315 	err error
316 }
317 
318 // WriteString writes the string to buffer, wrapping on a previous Space call if necessary.
319 // It never returns an error, all errors are held until Flush is called.
320 func (n *ninjaWriterWithWrap) WriteString(s string) (written int, noError error) {
321 	// Always return the full length of the string and a nil error.
322 	// ninjaWriterWithWrap doesn't return errors to the caller, it saves them until Flush()
323 	written = len(s)
324 
325 	if n.err != nil {
326 		return
327 	}
328 
329 	const spaceLen = 1
330 	if !n.space {
331 		// No space is pending, so a line wrap can't be inserted before this, so just write
332 		// the string.
333 		n.lineLen += len(s)
334 		_, n.err = n.writer.WriteString(s)
335 	} else if n.lineLen+len(s)+spaceLen > n.maxLineLen {
336 		// A space is pending, and the pending strings plus the current string would exceed the
337 		// maximum line length.  Wrap and indent before the pending space and strings, then write
338 		// the pending and current strings.
339 		_, n.err = n.writer.WriteString(" $\n")
340 		if n.err != nil {
341 			return
342 		}
343 		_, n.err = n.writer.WriteString(indentString[:indentWidth*2])
344 		if n.err != nil {
345 			return
346 		}
347 		n.lineLen = indentWidth*2 + n.pendingLen
348 		s = strings.TrimLeftFunc(s, unicode.IsSpace)
349 		n.pending = append(n.pending, s)
350 		n.writePending()
351 
352 		n.space = false
353 	} else {
354 		// A space is pending but the current string would not reach the maximum line length,
355 		// add it to the pending list.
356 		n.pending = append(n.pending, s)
357 		n.pendingLen += len(s)
358 		n.lineLen += len(s)
359 	}
360 
361 	return
362 }
363 
364 // Space inserts a space that is also a possible wrapping point into the string.
365 func (n *ninjaWriterWithWrap) Space() {
366 	if n.err != nil {
367 		return
368 	}
369 	if n.space {
370 		// A space was already pending, and the space plus any strings written after the space did
371 		// not reach the maxmimum line length, so write out the old space and pending strings.
372 		_, n.err = n.writer.WriteString(" ")
373 		n.lineLen++
374 		n.writePending()
375 	}
376 	n.space = true
377 }
378 
379 // writePending writes out all the strings stored in pending and resets it.
380 func (n *ninjaWriterWithWrap) writePending() {
381 	if n.err != nil {
382 		return
383 	}
384 	for _, pending := range n.pending {
385 		_, n.err = n.writer.WriteString(pending)
386 		if n.err != nil {
387 			return
388 		}
389 	}
390 	// Reset the length of pending back to 0 without reducing its capacity to avoid reallocating
391 	// the backing array.
392 	n.pending = n.pending[:0]
393 	n.pendingLen = 0
394 }
395 
396 // WriteStringWithSpace is a helper that calls Space and WriteString.
397 func (n *ninjaWriterWithWrap) WriteStringWithSpace(s string) {
398 	n.Space()
399 	_, _ = n.WriteString(s)
400 }
401 
402 // Flush writes out any pending space or strings and then a newline.  It also returns any errors
403 // that have previously occurred.
404 func (n *ninjaWriterWithWrap) Flush() error {
405 	if n.space {
406 		_, n.err = n.writer.WriteString(" ")
407 	}
408 	n.writePending()
409 	if n.err != nil {
410 		return n.err
411 	}
412 	_, err := n.writer.WriteString("\n")
413 	return err
414 }
415