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, explicitDeps, implicitDeps,
107	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		wrapper.Comment(comment)
121	}
122
123	wrapper.WriteString("build")
124
125	for _, output := range outputs {
126		wrapper.WriteStringWithSpace(output)
127	}
128
129	wrapper.WriteString(":")
130
131	wrapper.WriteStringWithSpace(rule)
132
133	for _, dep := range explicitDeps {
134		wrapper.WriteStringWithSpace(dep)
135	}
136
137	if len(implicitDeps) > 0 {
138		wrapper.WriteStringWithSpace("|")
139
140		for _, dep := range implicitDeps {
141			wrapper.WriteStringWithSpace(dep)
142		}
143	}
144
145	if len(orderOnlyDeps) > 0 {
146		wrapper.WriteStringWithSpace("||")
147
148		for _, dep := range orderOnlyDeps {
149			wrapper.WriteStringWithSpace(dep)
150		}
151	}
152
153	return wrapper.Flush()
154}
155
156func (n *ninjaWriter) Assign(name, value string) error {
157	n.justDidBlankLine = false
158	_, err := fmt.Fprintf(n.writer, "%s = %s\n", name, value)
159	return err
160}
161
162func (n *ninjaWriter) ScopedAssign(name, value string) error {
163	n.justDidBlankLine = false
164	_, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indentString[:indentWidth], name, value)
165	return err
166}
167
168func (n *ninjaWriter) Default(targets ...string) error {
169	n.justDidBlankLine = false
170
171	const lineWrapLen = len(" $")
172	const maxLineLen = lineWidth - lineWrapLen
173
174	wrapper := ninjaWriterWithWrap{
175		ninjaWriter: n,
176		maxLineLen:  maxLineLen,
177	}
178
179	wrapper.WriteString("default")
180
181	for _, target := range targets {
182		wrapper.WriteString(" " + target)
183	}
184
185	return wrapper.Flush()
186}
187
188func (n *ninjaWriter) BlankLine() (err error) {
189	// We don't output multiple blank lines in a row.
190	if !n.justDidBlankLine {
191		n.justDidBlankLine = true
192		_, err = io.WriteString(n.writer, "\n")
193	}
194	return err
195}
196
197type ninjaWriterWithWrap struct {
198	*ninjaWriter
199	maxLineLen int
200	writtenLen int
201	err        error
202}
203
204func (n *ninjaWriterWithWrap) writeString(s string, space bool) {
205	if n.err != nil {
206		return
207	}
208
209	spaceLen := 0
210	if space {
211		spaceLen = 1
212	}
213
214	if n.writtenLen+len(s)+spaceLen > n.maxLineLen {
215		_, n.err = io.WriteString(n.writer, " $\n")
216		if n.err != nil {
217			return
218		}
219		_, n.err = io.WriteString(n.writer, indentString[:indentWidth*2])
220		if n.err != nil {
221			return
222		}
223		n.writtenLen = indentWidth * 2
224		s = strings.TrimLeftFunc(s, unicode.IsSpace)
225	} else if space {
226		io.WriteString(n.writer, " ")
227		n.writtenLen++
228	}
229
230	_, n.err = io.WriteString(n.writer, s)
231	n.writtenLen += len(s)
232}
233
234func (n *ninjaWriterWithWrap) WriteString(s string) {
235	n.writeString(s, false)
236}
237
238func (n *ninjaWriterWithWrap) WriteStringWithSpace(s string) {
239	n.writeString(s, true)
240}
241
242func (n *ninjaWriterWithWrap) Flush() error {
243	if n.err != nil {
244		return n.err
245	}
246	_, err := io.WriteString(n.writer, "\n")
247	return err
248}
249