blob: c351b93744a002032623b2ca1be514baa32cf7c2 [file] [log] [blame]
// 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 (
"bytes"
"fmt"
"io"
"slices"
"strings"
)
const eof = -1
var (
defaultEscaper = strings.NewReplacer(
"\n", "$\n")
inputEscaper = strings.NewReplacer(
"\n", "$\n",
" ", "$ ")
outputEscaper = strings.NewReplacer(
"\n", "$\n",
" ", "$ ",
":", "$:")
)
// ninjaString contains the parsed result of a string that can contain references to variables (e.g. $cflags) that will
// be propagated to the build.ninja file. For literal strings with no variable references, the variables field will be
// nil. For strings with variable references str contains the original, unparsed string, and variables contains a
// pointer to a list of references, each with a span of bytes they should replace and a Variable interface.
type ninjaString struct {
str string
variables *[]variableReference
}
// variableReference contains information about a single reference to a variable (e.g. $cflags) inside a parsed
// ninjaString. start and end are int32 to reduce memory usage. A nil variable is a special case of an inserted '$'
// at the beginning of the string to handle leading whitespace that must not be stripped by ninja.
type variableReference struct {
// start is the offset of the '$' character from the beginning of the unparsed string.
start int32
// end is the offset of the character _after_ the final character of the variable name (or '}' if using the
//'${}' syntax)
end int32
variable Variable
}
type scope interface {
LookupVariable(name string) (Variable, error)
IsRuleVisible(rule Rule) bool
IsPoolVisible(pool Pool) bool
}
func simpleNinjaString(str string) *ninjaString {
return &ninjaString{str: str}
}
type parseState struct {
scope scope
str string
varStart int
varNameStart int
result *ninjaString
}
func (ps *parseState) pushVariable(start, end int, v Variable) {
if ps.result.variables == nil {
ps.result.variables = &[]variableReference{{start: int32(start), end: int32(end), variable: v}}
} else {
*ps.result.variables = append(*ps.result.variables, variableReference{start: int32(start), end: int32(end), variable: v})
}
}
type stateFunc func(*parseState, int, rune) (stateFunc, error)
// parseNinjaString parses an unescaped ninja string (i.e. all $<something>
// occurrences are expected to be variables or $$) and returns a *ninjaString
// that contains the original string and a list of the referenced variables.
func parseNinjaString(scope scope, str string) (*ninjaString, error) {
ninjaString, str, err := parseNinjaOrSimpleString(scope, str)
if err != nil {
return nil, err
}
if ninjaString != nil {
return ninjaString, nil
}
return simpleNinjaString(str), nil
}
// parseNinjaOrSimpleString parses an unescaped ninja string (i.e. all $<something>
// occurrences are expected to be variables or $$) and returns either a *ninjaString
// if the string contains ninja variable references, or the original string and nil
// for the *ninjaString if it doesn't.
func parseNinjaOrSimpleString(scope scope, str string) (*ninjaString, string, error) {
// naively pre-allocate slice by counting $ signs
n := strings.Count(str, "$")
if n == 0 {
if len(str) > 0 && str[0] == ' ' {
str = "$" + str
}
return nil, str, nil
}
variableReferences := make([]variableReference, 0, n)
result := &ninjaString{
str: str,
variables: &variableReferences,
}
parseState := &parseState{
scope: scope,
str: str,
result: result,
}
state := parseFirstRuneState
var err error
for i := 0; i < len(str); i++ {
r := rune(str[i])
state, err = state(parseState, i, r)
if err != nil {
return nil, "", fmt.Errorf("error parsing ninja string %q: %s", str, err)
}
}
_, err = state(parseState, len(parseState.str), eof)
if err != nil {
return nil, "", err
}
// All the '$' characters counted initially could have been "$$" escapes, leaving no
// variable references. Deallocate the variables slice if so.
if len(*result.variables) == 0 {
result.variables = nil
}
return result, "", nil
}
func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) {
if r == ' ' {
state.pushVariable(0, 1, nil)
}
return parseStringState(state, i, r)
}
func parseStringState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r == '$':
state.varStart = i
return parseDollarStartState, nil
case r == eof:
return nil, nil
default:
return parseStringState, nil
}
}
func parseDollarStartState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-':
// The beginning of a of the variable name.
state.varNameStart = i
return parseDollarState, nil
case r == '$':
// Just a "$$". Go back to parseStringState.
return parseStringState, nil
case r == '{':
// This is a bracketted variable name (e.g. "${blah.blah}").
state.varNameStart = i + 1
return parseBracketsState, nil
case r == eof:
return nil, fmt.Errorf("unexpected end of string after '$'")
default:
// This was some arbitrary character following a dollar sign,
// which is not allowed.
return nil, fmt.Errorf("invalid character after '$' at byte "+
"offset %d", i)
}
}
func parseDollarState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-':
// A part of the variable name. Keep going.
return parseDollarState, nil
}
// The variable name has ended, output what we have.
v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
if err != nil {
return nil, err
}
state.pushVariable(state.varStart, i, v)
switch {
case r == '$':
// A dollar after the variable name (e.g. "$blah$"). Start a new one.
state.varStart = i
return parseDollarStartState, nil
case r == eof:
return nil, nil
default:
return parseStringState, nil
}
}
func parseBracketsState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-', r == '.':
// A part of the variable name. Keep going.
return parseBracketsState, nil
case r == '}':
if state.varNameStart == i {
// The brackets were immediately closed. That's no good.
return nil, fmt.Errorf("empty variable name at byte offset %d",
i)
}
// This is the end of the variable name.
v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
if err != nil {
return nil, err
}
state.pushVariable(state.varStart, i+1, v)
return parseStringState, nil
case r == eof:
return nil, fmt.Errorf("unexpected end of string in variable name")
default:
// This character isn't allowed in a variable name.
return nil, fmt.Errorf("invalid character in variable name at "+
"byte offset %d", i)
}
}
// parseNinjaStrings converts a list of strings to *ninjaStrings by finding the references
// to ninja variables contained in the strings.
func parseNinjaStrings(scope scope, strs []string) ([]*ninjaString,
error) {
if len(strs) == 0 {
return nil, nil
}
result := make([]*ninjaString, len(strs))
for i, str := range strs {
ninjaStr, err := parseNinjaString(scope, str)
if err != nil {
return nil, fmt.Errorf("error parsing element %d: %s", i, err)
}
result[i] = ninjaStr
}
return result, nil
}
// parseNinjaOrSimpleStrings splits a list of strings into *ninjaStrings if they have ninja
// variable references or a list of strings if they don't. If none of the input strings contain
// ninja variable references (a very common case) then it returns the unmodified input slice as
// the output slice.
func parseNinjaOrSimpleStrings(scope scope, strs []string) ([]*ninjaString, []string, error) {
if len(strs) == 0 {
return nil, strs, nil
}
// allSimpleStrings is true until the first time a string with ninja variable references is found.
allSimpleStrings := true
var simpleStrings []string
var ninjaStrings []*ninjaString
for i, str := range strs {
ninjaStr, simpleStr, err := parseNinjaOrSimpleString(scope, str)
if err != nil {
return nil, nil, fmt.Errorf("error parsing element %d: %s", i, err)
} else if ninjaStr != nil {
ninjaStrings = append(ninjaStrings, ninjaStr)
if allSimpleStrings && i > 0 {
// If all previous strings had no ninja variable references then they weren't copied into
// simpleStrings to avoid allocating it if the input slice is reused as the output. Allocate
// simpleStrings and copy all the previous strings into it.
simpleStrings = make([]string, i, len(strs))
copy(simpleStrings, strs[:i])
}
allSimpleStrings = false
} else {
if !allSimpleStrings {
// Only copy into the output slice if at least one string with ninja variable references
// was found. Skipped strings will be copied the first time a string with ninja variable
// is found.
simpleStrings = append(simpleStrings, simpleStr)
}
}
}
if allSimpleStrings {
// None of the input strings had ninja variable references, return the input slice as the output.
return nil, strs, nil
}
return ninjaStrings, simpleStrings, nil
}
func (n *ninjaString) Value(nameTracker *nameTracker) string {
if n.variables == nil || len(*n.variables) == 0 {
return defaultEscaper.Replace(n.str)
}
str := &strings.Builder{}
n.ValueWithEscaper(str, nameTracker, defaultEscaper)
return str.String()
}
func (n *ninjaString) ValueWithEscaper(w io.StringWriter, nameTracker *nameTracker, escaper *strings.Replacer) {
if n.variables == nil || len(*n.variables) == 0 {
w.WriteString(escaper.Replace(n.str))
return
}
i := 0
for _, v := range *n.variables {
w.WriteString(escaper.Replace(n.str[i:v.start]))
if v.variable == nil {
w.WriteString("$ ")
} else {
w.WriteString("${")
w.WriteString(nameTracker.Variable(v.variable))
w.WriteString("}")
}
i = int(v.end)
}
w.WriteString(escaper.Replace(n.str[i:len(n.str)]))
}
func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) {
if n.variables == nil || len(*n.variables) == 0 {
return n.str, nil
}
w := &strings.Builder{}
i := 0
for _, v := range *n.variables {
w.WriteString(n.str[i:v.start])
if v.variable == nil {
w.WriteString(" ")
} else {
variable, ok := variables[v.variable]
if !ok {
return "", fmt.Errorf("no such global variable: %s", v.variable)
}
value, err := variable.Eval(variables)
if err != nil {
return "", err
}
w.WriteString(value)
}
i = int(v.end)
}
w.WriteString(n.str[i:len(n.str)])
return w.String(), nil
}
func (n *ninjaString) Variables() []Variable {
if n.variables == nil || len(*n.variables) == 0 {
return nil
}
variables := make([]Variable, 0, len(*n.variables))
for _, v := range *n.variables {
if v.variable != nil {
variables = append(variables, v.variable)
}
}
return variables
}
func validateNinjaName(name string) error {
for i, r := range name {
valid := (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
(r == '_') ||
(r == '-') ||
(r == '.')
if !valid {
return fmt.Errorf("%q contains an invalid Ninja name character "+
"%q at byte offset %d", name, r, i)
}
}
return nil
}
func toNinjaName(name string) string {
ret := bytes.Buffer{}
ret.Grow(len(name))
for _, r := range name {
valid := (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
(r == '_') ||
(r == '-') ||
(r == '.')
if valid {
ret.WriteRune(r)
} else {
// TODO(jeffrygaston): do escaping so that toNinjaName won't ever output duplicate
// names for two different input names
ret.WriteRune('_')
}
}
return ret.String()
}
var builtinRuleArgs = []string{"out", "in"}
func validateArgName(argName string) error {
err := validateNinjaName(argName)
if err != nil {
return err
}
// We only allow globals within the rule's package to be used as rule
// arguments. A global in another package can always be mirrored into
// the rule's package by defining a new variable, so this doesn't limit
// what's possible. This limitation prevents situations where a Build
// invocation in another package must use the rule-defining package's
// import name for a 3rd package in order to set the rule's arguments.
if strings.ContainsRune(argName, '.') {
return fmt.Errorf("%q contains a '.' character", argName)
}
if argName == "tags" {
return fmt.Errorf("\"tags\" is a reserved argument name")
}
for _, builtin := range builtinRuleArgs {
if argName == builtin {
return fmt.Errorf("%q conflicts with Ninja built-in", argName)
}
}
return nil
}
func validateArgNames(argNames []string) error {
for _, argName := range argNames {
err := validateArgName(argName)
if err != nil {
return err
}
}
return nil
}
func ninjaStringsEqual(a, b *ninjaString) bool {
return a.str == b.str &&
(a.variables == nil) == (b.variables == nil) &&
(a.variables == nil ||
slices.Equal(*a.variables, *b.variables))
}