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	"fmt"
19	"io"
20	"strings"
21	"unicode"
22)
23
24const (
25	indentWidth    = 4
26	maxIndentDepth = 2
27	lineWidth      = 80
28)
29
30var indentString = strings.Repeat(" ", indentWidth*maxIndentDepth)
31
32type ninjaWriter struct {
33	writer io.Writer
34
35	justDidBlankLine bool // true if the last operation was a BlankLine
36}
37
38func newNinjaWriter(writer io.Writer) *ninjaWriter {
39	return &ninjaWriter{
40		writer: writer,
41	}
42}
43
44func (n *ninjaWriter) Comment(comment string) error {
45	n.justDidBlankLine = false
46
47	const lineHeaderLen = len("# ")
48	const maxLineLen = lineWidth - lineHeaderLen
49
50	var lineStart, lastSplitPoint int
51	for i, r := range comment {
52		if unicode.IsSpace(r) {
53			// We know we can safely split the line here.
54			lastSplitPoint = i + 1
55		}
56
57		var line string
58		var writeLine bool
59		switch {
60		case r == '\n':
61			// Output the line without trimming the left so as to allow comments
62			// to contain their own indentation.
63			line = strings.TrimRightFunc(comment[lineStart:i], unicode.IsSpace)
64			writeLine = true
65
66		case (i-lineStart > maxLineLen) && (lastSplitPoint > lineStart):
67			// The line has grown too long and is splittable.  Split it at the
68			// last split point.
69			line = strings.TrimSpace(comment[lineStart:lastSplitPoint])
70			writeLine = true
71		}
72
73		if writeLine {
74			line = strings.TrimSpace("# "+line) + "\n"
75			_, err := io.WriteString(n.writer, line)
76			if err != nil {
77				return err
78			}
79			lineStart = lastSplitPoint
80		}
81	}
82
83	if lineStart != len(comment) {
84		line := strings.TrimSpace(comment[lineStart:])
85		_, err := fmt.Fprintf(n.writer, "# %s\n", line)
86		if err != nil {
87			return err
88		}
89	}
90
91	return nil
92}
93
94func (n *ninjaWriter) Pool(name string) error {
95	n.justDidBlankLine = false
96	_, err := fmt.Fprintf(n.writer, "pool %s\n", name)
97	return err
98}
99
100func (n *ninjaWriter) Rule(name string) error {
101	n.justDidBlankLine = false
102	_, err := fmt.Fprintf(n.writer, "rule %s\n", name)
103	return err
104}
105
106func (n *ninjaWriter) Build(comment string, rule string, outputs, implicitOuts,
107	explicitDeps, implicitDeps, orderOnlyDeps []string) error {
108
109	n.justDidBlankLine = false
110
111	const lineWrapLen = len(" $")
112	const maxLineLen = lineWidth - lineWrapLen
113
114	wrapper := ninjaWriterWithWrap{
115		ninjaWriter: n,
116		maxLineLen:  maxLineLen,
117	}
118
119	if comment != "" {
120		err := wrapper.Comment(comment)
121		if err != nil {
122			return err
123		}
124	}
125
126	wrapper.WriteString("build")
127
128	for _, output := range outputs {
129		wrapper.WriteStringWithSpace(output)
130	}
131
132	if len(implicitOuts) > 0 {
133		wrapper.WriteStringWithSpace("|")
134
135		for _, out := range implicitOuts {
136			wrapper.WriteStringWithSpace(out)
137		}
138	}
139
140	wrapper.WriteString(":")
141
142	wrapper.WriteStringWithSpace(rule)
143
144	for _, dep := range explicitDeps {
145		wrapper.WriteStringWithSpace(dep)
146	}
147
148	if len(implicitDeps) > 0 {
149		wrapper.WriteStringWithSpace("|")
150
151		for _, dep := range implicitDeps {
152			wrapper.WriteStringWithSpace(dep)
153		}
154	}
155
156	if len(orderOnlyDeps) > 0 {
157		wrapper.WriteStringWithSpace("||")
158
159		for _, dep := range orderOnlyDeps {
160			wrapper.WriteStringWithSpace(dep)
161		}
162	}
163
164	return wrapper.Flush()
165}
166
167func (n *ninjaWriter) Assign(name, value string) error {
168	n.justDidBlankLine = false
169	_, err := fmt.Fprintf(n.writer, "%s = %s\n", name, value)
170	return err
171}
172
173func (n *ninjaWriter) ScopedAssign(name, value string) error {
174	n.justDidBlankLine = false
175	_, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indentString[:indentWidth], name, value)
176	return err
177}
178
179func (n *ninjaWriter) Default(targets ...string) error {
180	n.justDidBlankLine = false
181
182	const lineWrapLen = len(" $")
183	const maxLineLen = lineWidth - lineWrapLen
184
185	wrapper := ninjaWriterWithWrap{
186		ninjaWriter: n,
187		maxLineLen:  maxLineLen,
188	}
189
190	wrapper.WriteString("default")
191
192	for _, target := range targets {
193		wrapper.WriteString(" " + target)
194	}
195
196	return wrapper.Flush()
197}
198
199func (n *ninjaWriter) Subninja(file string) error {
200	n.justDidBlankLine = false
201	_, err := fmt.Fprintf(n.writer, "subninja %s\n", file)
202	return err
203}
204
205func (n *ninjaWriter) BlankLine() (err error) {
206	// We don't output multiple blank lines in a row.
207	if !n.justDidBlankLine {
208		n.justDidBlankLine = true
209		_, err = io.WriteString(n.writer, "\n")
210	}
211	return err
212}
213
214type ninjaWriterWithWrap struct {
215	*ninjaWriter
216	maxLineLen int
217	writtenLen int
218	err        error
219}
220
221func (n *ninjaWriterWithWrap) writeString(s string, space bool) {
222	if n.err != nil {
223		return
224	}
225
226	spaceLen := 0
227	if space {
228		spaceLen = 1
229	}
230
231	if n.writtenLen+len(s)+spaceLen > n.maxLineLen {
232		_, n.err = io.WriteString(n.writer, " $\n")
233		if n.err != nil {
234			return
235		}
236		_, n.err = io.WriteString(n.writer, indentString[:indentWidth*2])
237		if n.err != nil {
238			return
239		}
240		n.writtenLen = indentWidth * 2
241		s = strings.TrimLeftFunc(s, unicode.IsSpace)
242	} else if space {
243		_, n.err = io.WriteString(n.writer, " ")
244		if n.err != nil {
245			return
246		}
247		n.writtenLen++
248	}
249
250	_, n.err = io.WriteString(n.writer, s)
251	n.writtenLen += len(s)
252}
253
254func (n *ninjaWriterWithWrap) WriteString(s string) {
255	n.writeString(s, false)
256}
257
258func (n *ninjaWriterWithWrap) WriteStringWithSpace(s string) {
259	n.writeString(s, true)
260}
261
262func (n *ninjaWriterWithWrap) Flush() error {
263	if n.err != nil {
264		return n.err
265	}
266	_, err := io.WriteString(n.writer, "\n")
267	return err
268}
269