// Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package blueprint import ( "io" "strings" "unicode" ) const ( indentWidth = 4 maxIndentDepth = 2 lineWidth = 80 ) var indentString = strings.Repeat(" ", indentWidth*maxIndentDepth) type StringWriterWriter interface { io.StringWriter io.Writer } type ninjaWriter struct { writer io.StringWriter justDidBlankLine bool // true if the last operation was a BlankLine } func newNinjaWriter(writer io.StringWriter) *ninjaWriter { return &ninjaWriter{ writer: writer, } } func (n *ninjaWriter) Comment(comment string) error { n.justDidBlankLine = false const lineHeaderLen = len("# ") const maxLineLen = lineWidth - lineHeaderLen var lineStart, lastSplitPoint int for i, r := range comment { if unicode.IsSpace(r) { // We know we can safely split the line here. lastSplitPoint = i + 1 } var line string var writeLine bool switch { case r == '\n': // Output the line without trimming the left so as to allow comments // to contain their own indentation. line = strings.TrimRightFunc(comment[lineStart:i], unicode.IsSpace) writeLine = true case (i-lineStart > maxLineLen) && (lastSplitPoint > lineStart): // The line has grown too long and is splittable. Split it at the // last split point. line = strings.TrimSpace(comment[lineStart:lastSplitPoint]) writeLine = true } if writeLine { line = strings.TrimSpace("# "+line) + "\n" _, err := n.writer.WriteString(line) if err != nil { return err } lineStart = lastSplitPoint } } if lineStart != len(comment) { line := strings.TrimSpace(comment[lineStart:]) _, err := n.writer.WriteString("# ") if err != nil { return err } _, err = n.writer.WriteString(line) if err != nil { return err } _, err = n.writer.WriteString("\n") if err != nil { return err } } return nil } func (n *ninjaWriter) Pool(name string) error { n.justDidBlankLine = false return n.writeStatement("pool", name) } func (n *ninjaWriter) Rule(name string) error { n.justDidBlankLine = false return n.writeStatement("rule", name) } func (n *ninjaWriter) Build(comment string, rule string, outputs, implicitOuts, explicitDeps, implicitDeps, orderOnlyDeps, validations []*ninjaString, pkgNames map[*packageContext]string) error { n.justDidBlankLine = false const lineWrapLen = len(" $") const maxLineLen = lineWidth - lineWrapLen wrapper := &ninjaWriterWithWrap{ ninjaWriter: n, maxLineLen: maxLineLen, } if comment != "" { err := wrapper.Comment(comment) if err != nil { return err } } wrapper.WriteString("build") for _, output := range outputs { wrapper.Space() output.ValueWithEscaper(wrapper, pkgNames, outputEscaper) } if len(implicitOuts) > 0 { wrapper.WriteStringWithSpace("|") for _, out := range implicitOuts { wrapper.Space() out.ValueWithEscaper(wrapper, pkgNames, outputEscaper) } } wrapper.WriteString(":") wrapper.WriteStringWithSpace(rule) for _, dep := range explicitDeps { wrapper.Space() dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper) } if len(implicitDeps) > 0 { wrapper.WriteStringWithSpace("|") for _, dep := range implicitDeps { wrapper.Space() dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper) } } if len(orderOnlyDeps) > 0 { wrapper.WriteStringWithSpace("||") for _, dep := range orderOnlyDeps { wrapper.Space() dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper) } } if len(validations) > 0 { wrapper.WriteStringWithSpace("|@") for _, dep := range validations { wrapper.Space() dep.ValueWithEscaper(wrapper, pkgNames, inputEscaper) } } return wrapper.Flush() } func (n *ninjaWriter) Assign(name, value string) error { n.justDidBlankLine = false _, err := n.writer.WriteString(name) if err != nil { return err } _, err = n.writer.WriteString(" = ") if err != nil { return err } _, err = n.writer.WriteString(value) if err != nil { return err } _, err = n.writer.WriteString("\n") if err != nil { return err } return nil } func (n *ninjaWriter) ScopedAssign(name, value string) error { n.justDidBlankLine = false _, err := n.writer.WriteString(indentString[:indentWidth]) if err != nil { return err } _, err = n.writer.WriteString(name) if err != nil { return err } _, err = n.writer.WriteString(" = ") if err != nil { return err } _, err = n.writer.WriteString(value) if err != nil { return err } _, err = n.writer.WriteString("\n") if err != nil { return err } return nil } func (n *ninjaWriter) Default(pkgNames map[*packageContext]string, targets ...*ninjaString) error { n.justDidBlankLine = false const lineWrapLen = len(" $") const maxLineLen = lineWidth - lineWrapLen wrapper := &ninjaWriterWithWrap{ ninjaWriter: n, maxLineLen: maxLineLen, } wrapper.WriteString("default") for _, target := range targets { wrapper.Space() target.ValueWithEscaper(wrapper, pkgNames, outputEscaper) } return wrapper.Flush() } func (n *ninjaWriter) Subninja(file string) error { n.justDidBlankLine = false return n.writeStatement("subninja", file) } func (n *ninjaWriter) BlankLine() (err error) { // We don't output multiple blank lines in a row. if !n.justDidBlankLine { n.justDidBlankLine = true _, err = n.writer.WriteString("\n") } return err } func (n *ninjaWriter) writeStatement(directive, name string) error { _, err := n.writer.WriteString(directive + " ") if err != nil { return err } _, err = n.writer.WriteString(name) if err != nil { return err } _, err = n.writer.WriteString("\n") if err != nil { return err } return nil } // ninjaWriterWithWrap is an io.StringWriter that writes through to a ninjaWriter, but supports // user-readable line wrapping on boundaries when ninjaWriterWithWrap.Space is called. // It collects incoming calls to WriteString until either the line length is exceeded, in which case // it inserts a wrap before the pending strings and then writes them, or the next call to Space, in // which case it writes out the pending strings. // // WriteString never returns an error, all errors are held until Flush is called. Once an error has // occurred all writes become noops. type ninjaWriterWithWrap struct { *ninjaWriter // pending lists the strings that have been written since the last call to Space. pending []string // pendingLen accumulates the lengths of the strings in pending. pendingLen int // lineLen accumulates the number of bytes on the current line. lineLen int // maxLineLen is the length of the line before wrapping. maxLineLen int // space is true if the strings in pending should be preceded by a space. space bool // err holds any error that has occurred to return in Flush. err error } // WriteString writes the string to buffer, wrapping on a previous Space call if necessary. // It never returns an error, all errors are held until Flush is called. func (n *ninjaWriterWithWrap) WriteString(s string) (written int, noError error) { // Always return the full length of the string and a nil error. // ninjaWriterWithWrap doesn't return errors to the caller, it saves them until Flush() written = len(s) if n.err != nil { return } const spaceLen = 1 if !n.space { // No space is pending, so a line wrap can't be inserted before this, so just write // the string. n.lineLen += len(s) _, n.err = n.writer.WriteString(s) } else if n.lineLen+len(s)+spaceLen > n.maxLineLen { // A space is pending, and the pending strings plus the current string would exceed the // maximum line length. Wrap and indent before the pending space and strings, then write // the pending and current strings. _, n.err = n.writer.WriteString(" $\n") if n.err != nil { return } _, n.err = n.writer.WriteString(indentString[:indentWidth*2]) if n.err != nil { return } n.lineLen = indentWidth*2 + n.pendingLen s = strings.TrimLeftFunc(s, unicode.IsSpace) n.pending = append(n.pending, s) n.writePending() n.space = false } else { // A space is pending but the current string would not reach the maximum line length, // add it to the pending list. n.pending = append(n.pending, s) n.pendingLen += len(s) n.lineLen += len(s) } return } // Space inserts a space that is also a possible wrapping point into the string. func (n *ninjaWriterWithWrap) Space() { if n.err != nil { return } if n.space { // A space was already pending, and the space plus any strings written after the space did // not reach the maxmimum line length, so write out the old space and pending strings. _, n.err = n.writer.WriteString(" ") n.lineLen++ n.writePending() } n.space = true } // writePending writes out all the strings stored in pending and resets it. func (n *ninjaWriterWithWrap) writePending() { if n.err != nil { return } for _, pending := range n.pending { _, n.err = n.writer.WriteString(pending) if n.err != nil { return } } // Reset the length of pending back to 0 without reducing its capacity to avoid reallocating // the backing array. n.pending = n.pending[:0] n.pendingLen = 0 } // WriteStringWithSpace is a helper that calls Space and WriteString. func (n *ninjaWriterWithWrap) WriteStringWithSpace(s string) { n.Space() _, _ = n.WriteString(s) } // Flush writes out any pending space or strings and then a newline. It also returns any errors // that have previously occurred. func (n *ninjaWriterWithWrap) Flush() error { if n.space { _, n.err = n.writer.WriteString(" ") } n.writePending() if n.err != nil { return n.err } _, err := n.writer.WriteString("\n") return err }