| // 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 |
| } |