You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
10 KiB

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