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
15package blueprint
16
17import (
18	"io"
19	"strings"
20	"unicode"
21)
22
23const (
24	indentWidth    = 4
25	maxIndentDepth = 2
26	lineWidth      = 80
27)
28
29var indentString = strings.Repeat(" ", indentWidth*maxIndentDepth)
30
31type StringWriterWriter interface {
32	io.StringWriter
33	io.Writer
34}
35
36type ninjaWriter struct {
37	writer io.StringWriter
38
39	justDidBlankLine bool // true if the last operation was a BlankLine
40}
41
42func newNinjaWriter(writer io.StringWriter) *ninjaWriter {
43	return &ninjaWriter{
44		writer: writer,
45	}
46}
47
48func (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
106func (n *ninjaWriter) Pool(name string) error {
107	n.justDidBlankLine = false
108	return n.writeStatement("pool", name)
109}
110
111func (n *ninjaWriter) Rule(name string) error {
112	n.justDidBlankLine = false
113	return n.writeStatement("rule", name)
114}
115
116func (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
192func (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
213func (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
238func (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
259func (n *ninjaWriter) Subninja(file string) error {
260	n.justDidBlankLine = false
261	return n.writeStatement("subninja", file)
262}
263
264func (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
273func (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.
297type 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.
320func (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.
365func (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.
380func (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.
397func (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.
404func (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