Skip to content

Commit 22b7901

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 22b7901

File tree

3 files changed

+330
-36
lines changed

3 files changed

+330
-36
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: 145 additions & 36 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"
@@ -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,27 @@ 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+
ndata, err := cleanAndBuildify(opts, f)
12431221
if err != nil {
12441222
return &rewriteResult{file: name, errs: []error{fmt.Errorf("running buildifier: %v", err)}, records: records}
12451223
}
@@ -1264,6 +1242,58 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
12641242
return &rewriteResult{file: name, errs: errs, modified: true, records: records}
12651243
}
12661244

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

0 commit comments

Comments
 (0)