Skip to content

Commit d52ea0f

Browse files
author
Tim Malmström
committed
Adding function to execute Buildozer commands on a single in-memory file
A buildozer client are looking to execute the commands of buildozer on the content of a build file, in a service environment (without direct access to the file storage). The new function enables this by running the commands without accessing the file system.
1 parent d9ed52a commit d52ea0f

File tree

3 files changed

+333
-37
lines changed

3 files changed

+333
-37
lines changed

buildozer/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,25 @@ Buildozer commands can be made executable by means of a shebang line, too:
350350
add deps //base //strings|-:foo|-:bar
351351
```
352352

353+
## Using Buildozer in-memory
354+
355+
Some clients of Buildozer have the need to execute buildozer actions in memory
356+
(due to a service environment which does not have access to their file system).
357+
This can be done using, `edit.ExecuteCommandsOnInlineFile`, which accepts
358+
commands and BUILD file content as bytes, applies the changes and returns the
359+
raw file content.
360+
For more details and implementation, see [`/edit/buildozer.go`](../edit/buildozer.go)
361+
362+
Some caveats of running Buildozer in-memory:
363+
364+
* The function assumes (and validates to some extent) that all commands apply to
365+
the same file and will return errors if there are commands affecting different
366+
paths.
367+
* When referencing targets, the function will not reliably determine if targets
368+
are local or remote. Hence redundant path references may be included in
369+
output. (e.g. `add dep //package/path:bar|//package/path:foo` would add the dep
370+
`//package/path:bar` instead of just `:bar`).
371+
353372
## Error code
354373

355374
The return code is:

edit/buildozer.go

Lines changed: 148 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"io"
2727
"log"
2828
"os"
29+
"path"
2930
"path/filepath"
3031
"regexp"
3132
"runtime"
@@ -1104,7 +1105,7 @@ func getGlobalVariables(exprs []build.Expr) (vars map[string]*build.AssignExpr)
11041105

11051106
// BuildFileNames is exported so that users that want to override it
11061107
// in scripts are free to do so.
1107-
var BuildFileNames = [...]string{"BUILD.bazel", "BUILD", "BUCK"}
1108+
var BuildFileNames = [...]string{"BUILD", "BUILD.bazel", "BUCK"}
11081109

11091110
// Buildifier formats the build file using the buildifier logic.
11101111
type Buildifier interface {
@@ -1183,11 +1184,9 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
11831184
}
11841185
var errs []error
11851186
changed := false
1186-
for _, commands := range commandsForFile.commands {
1187-
target := commands.target
1188-
commands := commands.commands
1189-
_, _, absPkg, rule := InterpretLabelForWorkspaceLocation(opts.RootDir, target)
1190-
if label := labels.Parse(target); label.Package == stdinPackageName {
1187+
for _, cft := range commandsForFile.commands {
1188+
_, _, absPkg, rule := InterpretLabelForWorkspaceLocation(opts.RootDir, cft.target)
1189+
if label := labels.Parse(cft.target); label.Package == stdinPackageName {
11911190
// Special-case: This is already absolute
11921191
absPkg = stdinPackageName
11931192
}
@@ -1198,48 +1197,28 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
11981197

11991198
targets, err := expandTargets(f, rule)
12001199
if err != nil {
1201-
cerr := commandError(commands, target, err)
1200+
cerr := commandError(cft.commands, cft.target, err)
12021201
errs = append(errs, cerr)
12031202
if !opts.KeepGoing {
12041203
return &rewriteResult{file: name, errs: errs, records: records}
12051204
}
12061205
}
12071206
targets = filterRules(opts, targets)
1208-
for _, cmd := range commands {
1209-
cmdInfo := AllCommands[cmd.tokens[0]]
1210-
// Depending on whether a transformation is rule-specific or not, it should be applied to
1211-
// every rule that satisfies the filter or just once to the file.
1212-
cmdTargets := targets
1213-
if !cmdInfo.PerRule {
1214-
cmdTargets = []*build.Rule{nil}
1215-
}
1216-
for _, r := range cmdTargets {
1217-
record := &apipb.Output_Record{}
1218-
newf, err := cmdInfo.Fn(opts, CmdEnvironment{f, r, vars, absPkg, cmd.tokens[1:], record})
1219-
if len(record.Fields) != 0 {
1220-
records = append(records, record)
1221-
}
1222-
if err != nil {
1223-
cerr := commandError([]command{cmd}, target, err)
1224-
if opts.KeepGoing {
1225-
errs = append(errs, cerr)
1226-
} else {
1227-
return &rewriteResult{file: name, errs: []error{cerr}, records: records}
1228-
}
1229-
}
1230-
if newf != nil {
1231-
changed = true
1232-
f = newf
1233-
}
1234-
}
1207+
1208+
newf, err := executeCommandsInFile(opts, f, cft, targets, &records, vars, absPkg, &errs)
1209+
if err != nil {
1210+
return &rewriteResult{file: name, errs: []error{err}, records: records}
1211+
}
1212+
if newf != nil {
1213+
changed = true
1214+
f = newf
12351215
}
12361216
}
12371217
if !changed {
12381218
return &rewriteResult{file: name, errs: errs, records: records}
12391219
}
1240-
f = RemoveEmptyPackage(f)
1241-
f = RemoveEmptyUseRepoCalls(f)
1242-
ndata, err := buildifier.Buildify(opts, f)
1220+
1221+
ndata, err := cleanAndBuildify(opts, f)
12431222
if err != nil {
12441223
return &rewriteResult{file: name, errs: []error{fmt.Errorf("running buildifier: %v", err)}, records: records}
12451224
}
@@ -1264,6 +1243,58 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
12641243
return &rewriteResult{file: name, errs: errs, modified: true, records: records}
12651244
}
12661245

1246+
// executeCommandsInFile executes the provided commandsForTarget in the provided build.File.
1247+
func executeCommandsInFile(
1248+
opts *Options,
1249+
f *build.File,
1250+
cft commandsForTarget,
1251+
rules []*build.Rule,
1252+
records *[]*apipb.Output_Record,
1253+
vars map[string]*build.AssignExpr,
1254+
absPkg string,
1255+
errs *[]error,
1256+
) (*build.File, error) {
1257+
changed := false
1258+
for _, cmd := range cft.commands {
1259+
cmdInfo := AllCommands[cmd.tokens[0]]
1260+
// Depending on whether a transformation is rule-specific or not, it should be applied to
1261+
// every rule that satisfies the filter or just once to the file.
1262+
cmdTargets := rules
1263+
if !cmdInfo.PerRule {
1264+
cmdTargets = []*build.Rule{nil}
1265+
}
1266+
for _, r := range cmdTargets {
1267+
record := &apipb.Output_Record{}
1268+
newf, err := cmdInfo.Fn(opts, CmdEnvironment{f, r, vars, absPkg, cmd.tokens[1:], record})
1269+
if len(record.Fields) != 0 {
1270+
*records = append(*records, record)
1271+
}
1272+
if err != nil {
1273+
cerr := commandError([]command{cmd}, cft.target, err)
1274+
if opts.KeepGoing {
1275+
*errs = append(*errs, cerr)
1276+
} else {
1277+
return nil, cerr
1278+
}
1279+
}
1280+
if newf != nil {
1281+
f = newf
1282+
changed = true
1283+
}
1284+
}
1285+
}
1286+
if changed {
1287+
return f, nil
1288+
}
1289+
return nil, nil
1290+
}
1291+
1292+
func cleanAndBuildify(opts *Options, f *build.File) ([]byte, error) {
1293+
f = RemoveEmptyPackage(f)
1294+
f = RemoveEmptyUseRepoCalls(f)
1295+
return buildifier.Buildify(opts, f)
1296+
}
1297+
12671298
// EditFile is a function that does any prework needed before editing a file.
12681299
// e.g. "checking out for write" from a locking source control repo.
12691300
var EditFile = func(fi os.FileInfo, name string) error {
@@ -1547,3 +1578,83 @@ func Buildozer(opts *Options, args []string) int {
15471578
}
15481579
return 0
15491580
}
1581+
1582+
// ExecuteCommandsOnInlineFile executes the given commands on the given file content.
1583+
// Returns the new file content after applying the commands.
1584+
func ExecuteCommandsOnInlineFile(fileContent []byte, commands []string) ([]byte, error) {
1585+
opts := Options{}
1586+
commandsByTargetName, filename, err := groupCommandsForInlineFile(commands, opts)
1587+
if err != nil {
1588+
return nil, err
1589+
}
1590+
f, err := build.Parse(*filename, fileContent)
1591+
if err != nil {
1592+
return nil, err
1593+
}
1594+
if f.Type == build.TypeDefault {
1595+
// Buildozer is unable to infer the file type, fall back to BUILD by default.
1596+
f.Type = build.TypeBuild
1597+
}
1598+
for _, cft := range commandsByTargetName {
1599+
rules, err := expandTargets(f, cft.target)
1600+
if err != nil {
1601+
return nil, err
1602+
}
1603+
newf, err := executeCommandsInFile(
1604+
&opts,
1605+
f,
1606+
cft,
1607+
rules,
1608+
// Output records are ignored in inline file execution.
1609+
nil,
1610+
// Global variables not supported in inline file execution.
1611+
nil,
1612+
f.Pkg,
1613+
// Errors-list is ignored since opts.keepGoing is always false.
1614+
nil,
1615+
)
1616+
if err != nil {
1617+
return nil, err
1618+
}
1619+
if newf != nil {
1620+
f = newf
1621+
}
1622+
}
1623+
1624+
outputFileContent, err := cleanAndBuildify(&opts, f)
1625+
if err != nil {
1626+
return nil, err
1627+
}
1628+
return outputFileContent, nil
1629+
}
1630+
1631+
// groupCommandsForInlineFile groups the given commands by file and returns the commands for a
1632+
// single file. Returns an error if the commands modify multiple files or if commands are invalid.
1633+
func groupCommandsForInlineFile(commands []string, opts Options) ([]commandsForTarget, *string, error) {
1634+
commandsByFile := make(map[string][]commandsForTarget)
1635+
commandReader := strings.NewReader(strings.Join(commands, "\n"))
1636+
err := appendCommandsFromReader(&opts, commandReader, commandsByFile, nil)
1637+
if err != nil {
1638+
return nil, nil, fmt.Errorf("error parsing commands %s", err)
1639+
}
1640+
if len(commandsByFile) != 1 {
1641+
return nil, nil, fmt.Errorf("invalid input commands, expected all commands to reference a single file")
1642+
}
1643+
for filepath, commandsForTargets := range commandsByFile {
1644+
for i := range commandsForTargets {
1645+
cft := &commandsForTargets[i]
1646+
splitTarget := strings.Split(cft.target, ":")
1647+
switch len(splitTarget) {
1648+
case 1: // No-op
1649+
case 2:
1650+
// Only keeps target (what is after the ":" character) since path is redundant.
1651+
cft.target = splitTarget[1]
1652+
default:
1653+
return nil, nil, fmt.Errorf("invalid target name %q", cft.target)
1654+
}
1655+
}
1656+
_, filename := path.Split(filepath)
1657+
return commandsForTargets, &filename, nil
1658+
}
1659+
panic("unreachable")
1660+
}

0 commit comments

Comments
 (0)