Skip to content

Commit 2a2caee

Browse files
committed
feat: Make Build Entrypoint configurable
Adds optional `build_entrypoint` fields to the `build_rule` builtin to enable us to use binaries directly when building and support other cross-platform shells (e.g. nushell).
1 parent 0e1002f commit 2a2caee

File tree

11 files changed

+426
-17
lines changed

11 files changed

+426
-17
lines changed

rules/builtins.build_defs

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,65 @@
22

33

44
# Do not change the order of arguments to this function without updating the iota in targets.go to match it.
5-
def build_rule(name:str, cmd:str|dict='', test_cmd:str|dict='', debug_cmd:str='', srcs:list|dict=None, data:list|dict=None,
6-
debug_data:list|dict=None, outs:list|dict=None, deps:list=None, exported_deps:list=None, secrets:list|dict=None,
7-
tools:str|list|dict=None, test_tools:str|list|dict=None, debug_tools:str|list|dict=None, labels:list=None,
8-
visibility:list=CONFIG.DEFAULT_VISIBILITY, hashes:list=None, binary:bool=False, test:bool=False,
9-
test_only:bool=CONFIG.DEFAULT_TESTONLY, building_description:str=None, needs_transitive_deps:bool=False,
10-
output_is_complete:bool=False, sandbox:bool=CONFIG.BUILD_SANDBOX, test_sandbox:bool=CONFIG.TEST_SANDBOX,
11-
no_test_output:bool=False, flaky:bool|int=0, build_timeout:int|str=0, test_timeout:int|str=0, pre_build:function=None,
12-
post_build:function=None, requires:list=None, provides:dict=None, licences:list=CONFIG.DEFAULT_LICENCES,
13-
test_outputs:list=None, system_srcs:list=None, stamp:bool=False, tag:str='', optional_outs:list=None, progress:bool=False,
14-
size:str=None, _urls:list=None, internal_deps:list=None, pass_env:list=None, local:bool=False, output_dirs:list=[],
15-
exit_on_error:bool=CONFIG.EXIT_ON_ERROR, entry_points:dict={}, env:dict={}, _file_content:str=None,
16-
_subrepo:bool=False, no_test_coverage:bool=False):
5+
def build_rule(
6+
name:str,
7+
cmd:str|dict="",
8+
test_cmd:str|dict="",
9+
debug_cmd:str="",
10+
srcs:list|dict=None,
11+
data:list|dict=None,
12+
debug_data:list|dict=None,
13+
outs:list|dict=None,
14+
deps:list=None,
15+
exported_deps:list=None,
16+
secrets:list|dict=None,
17+
tools:str|list|dict=None,
18+
test_tools:str|list|dict=None,
19+
debug_tools:str|list|dict=None,
20+
labels:list=None,
21+
visibility:list=CONFIG.DEFAULT_VISIBILITY,
22+
hashes:list=None,
23+
binary:bool=False,
24+
test:bool=False,
25+
test_only:bool=CONFIG.DEFAULT_TESTONLY,
26+
building_description:str=None,
27+
needs_transitive_deps:bool=False,
28+
output_is_complete:bool=False,
29+
sandbox:bool=CONFIG.BUILD_SANDBOX,
30+
test_sandbox:bool=CONFIG.TEST_SANDBOX,
31+
no_test_output:bool=False,
32+
flaky:bool|int=0,
33+
build_timeout:int|str=0,
34+
test_timeout:int|str=0,
35+
pre_build:function=None,
36+
post_build:function=None,
37+
requires:list=None,
38+
provides:dict=None,
39+
licences:list=CONFIG.DEFAULT_LICENCES,
40+
test_outputs:list=None,
41+
system_srcs:list=None,
42+
stamp:bool=False,
43+
tag:str="",
44+
optional_outs:list=None,
45+
progress:bool=False,
46+
size:str=None,
47+
_urls:list=None,
48+
internal_deps:list=None,
49+
pass_env:list=None,
50+
local:bool=False,
51+
output_dirs:list=[],
52+
exit_on_error:bool=CONFIG.EXIT_ON_ERROR,
53+
entry_points:dict={},
54+
env:dict={},
55+
_file_content:str=None,
56+
_subrepo:bool=False,
57+
no_test_coverage:bool=False,
58+
# This matches the default `BuildEntrypoint` is defined in
59+
#`src/core/build_entrypoint.go`.
60+
build_entry_point:list=None,
61+
build_entry_point_exit_on_error_args:list=None,
62+
build_entry_point_interactive_args:list=None,
63+
build_entry_point_exec_command_args:list=None):
1764
pass
1865

1966
def chr(i:int) -> str:

src/build/build_step.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package build
33

44
import (
55
"bytes"
6+
"context"
67
"encoding/hex"
78
"errors"
89
"fmt"
@@ -515,12 +516,28 @@ func runBuildCommand(state *core.BuildState, target *core.BuildTarget, command s
515516
if target.IsTextFile {
516517
return nil, buildTextFile(state, target)
517518
}
519+
518520
env := core.StampedBuildEnvironment(state, target, inputHash, filepath.Join(core.RepoRoot, target.TmpDir()), target.Stamp).ToSlice()
519521
log.Debug("Building target %s\nENVIRONMENT:\n%s\n%s", target.Label, env, command)
520-
out, combined, err := state.ProcessExecutor.ExecWithTimeoutShell(target, target.TmpDir(), env, target.BuildTimeout, state.ShowAllOutput, false, process.NewSandboxConfig(target.Sandbox, target.Sandbox), command)
522+
523+
buildArgvOpts := []core.BuildArgvOpt{target.BuildEntryPoint.WithBuildArgvCommand(command)}
524+
if target.ShouldExitOnError() {
525+
buildArgvOpts = append(buildArgvOpts, target.BuildEntryPoint.WithBuildArgvExitOnError())
526+
}
527+
argv, err := target.BuildEntryPoint.BuildArgv(state, target, buildArgvOpts...)
528+
if err != nil {
529+
return nil, err
530+
}
531+
out, combined, err := state.ProcessExecutor.ExecWithTimeout(
532+
context.Background(),
533+
target, target.TmpDir(), env, target.BuildTimeout, state.ShowAllOutput,
534+
false, false, false, process.NewSandboxConfig(target.Sandbox, target.Sandbox),
535+
argv,
536+
)
521537
if err != nil {
522538
return nil, fmt.Errorf("Error building target %s: %s\n%s", target.Label, err, combined)
523539
}
540+
524541
return out, nil
525542
}
526543

src/build/incrementality_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var KnownFields = map[string]bool{
2626
"IsTextFile": true,
2727
"FileContent": true,
2828
"IsRemoteFile": true,
29+
"BuildEntryPoint": true,
2930
"Command": true,
3031
"Commands": true,
3132
"NeedsTransitiveDependencies": true,

src/core/build_entrypoint.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package core
2+
3+
type BuildEntrypoint struct {
4+
Entrypoint []string
5+
ExecCommandArgs []string
6+
ExitOnErrorArgs []string
7+
InteractiveArgs []string
8+
}
9+
10+
type BuildEntrypointOpt func(*BuildEntrypoint)
11+
12+
func WithBuildEntrypointEntrypoint(entrypoint []string) BuildEntrypointOpt {
13+
return func(be *BuildEntrypoint) {
14+
be.Entrypoint = entrypoint
15+
}
16+
}
17+
18+
func WithBuildEntrypointExitOnErrorArgs(args []string) BuildEntrypointOpt {
19+
return func(be *BuildEntrypoint) {
20+
be.ExitOnErrorArgs = args
21+
}
22+
}
23+
24+
func WithBuildEntrypointExecCommandArgs(args []string) BuildEntrypointOpt {
25+
return func(be *BuildEntrypoint) {
26+
be.ExecCommandArgs = args
27+
}
28+
}
29+
30+
func WithBuildEntrypointInteractiveArgs(args []string) BuildEntrypointOpt {
31+
return func(be *BuildEntrypoint) {
32+
be.InteractiveArgs = args
33+
}
34+
}
35+
36+
func NewBuildEntrypoint(opts ...BuildEntrypointOpt) *BuildEntrypoint {
37+
be := &BuildEntrypoint{
38+
Entrypoint: []string{},
39+
ExecCommandArgs: []string{},
40+
ExitOnErrorArgs: []string{},
41+
InteractiveArgs: []string{},
42+
}
43+
44+
for _, opt := range opts {
45+
opt(be)
46+
}
47+
48+
// Default to Bash if Entrypoint not set.
49+
if len(be.Entrypoint) < 1 {
50+
be.Entrypoint = []string{"bash", "--noprofile", "--norc", "-u", "-o", "pipefail"}
51+
be.ExecCommandArgs = []string{"-c"}
52+
be.ExitOnErrorArgs = []string{"-e"}
53+
be.InteractiveArgs = []string{}
54+
}
55+
56+
return be
57+
}
58+
59+
type BuildArgv struct{ Argv []string }
60+
type BuildArgvOpt func(*BuildArgv)
61+
62+
func (be *BuildEntrypoint) WithBuildArgvExitOnError() BuildArgvOpt {
63+
return func(ba *BuildArgv) {
64+
ba.Argv = append(ba.Argv, be.ExitOnErrorArgs...)
65+
}
66+
}
67+
68+
func (be *BuildEntrypoint) WithBuildArgvInteractive() BuildArgvOpt {
69+
return func(ba *BuildArgv) {
70+
log.Debugf("pre interactive argv: %#v", ba.Argv)
71+
ba.Argv = append(ba.Argv, be.InteractiveArgs...)
72+
73+
log.Debugf("post interactive argv: %#v", ba.Argv)
74+
}
75+
}
76+
77+
func (be *BuildEntrypoint) WithBuildArgvCommand(command string) BuildArgvOpt {
78+
return func(ba *BuildArgv) {
79+
ba.Argv = append(ba.Argv, append(be.ExecCommandArgs, command)...)
80+
}
81+
}
82+
83+
func (be *BuildEntrypoint) BuildArgv(buildState *BuildState, target *BuildTarget, opts ...BuildArgvOpt) ([]string, error) {
84+
argv := &BuildArgv{Argv: be.Entrypoint}
85+
for _, opt := range opts {
86+
opt(argv)
87+
}
88+
89+
newArg0, err := ReplaceSequences(buildState, target, argv.Argv[0])
90+
if err != nil {
91+
return nil, err
92+
}
93+
argv.Argv[0] = newArg0
94+
95+
return argv.Argv, nil
96+
}

src/core/build_entrypoint_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package core
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestNewBuildEntrypoint(t *testing.T) {
10+
var tests = []struct {
11+
description string
12+
opts []BuildEntrypointOpt
13+
expected *BuildEntrypoint
14+
}{
15+
{
16+
"DefaultConfigIsBash",
17+
nil,
18+
&BuildEntrypoint{
19+
Entrypoint: []string{"bash", "--noprofile", "--norc", "-u", "-o", "pipefail"},
20+
ExecCommandArgs: []string{"-c"},
21+
ExitOnErrorArgs: []string{"-e"},
22+
InteractiveArgs: []string{},
23+
},
24+
},
25+
{
26+
"NuShell",
27+
[]BuildEntrypointOpt{
28+
WithBuildEntrypointEntrypoint([]string{"nu", "--no-config-file", "--no-history"}),
29+
WithBuildEntrypointInteractiveArgs([]string{"--execute", "$env.config.show_banner = false"}),
30+
WithBuildEntrypointExecCommandArgs([]string{"--commands"}),
31+
},
32+
&BuildEntrypoint{
33+
Entrypoint: []string{"nu", "--no-config-file", "--no-history"},
34+
ExecCommandArgs: []string{"--commands"},
35+
ExitOnErrorArgs: []string{},
36+
InteractiveArgs: []string{"--execute", "$env.config.show_banner = false"},
37+
},
38+
},
39+
{
40+
"Powershell",
41+
[]BuildEntrypointOpt{
42+
WithBuildEntrypointEntrypoint([]string{"pwsh", "-NoProfile"}),
43+
WithBuildEntrypointInteractiveArgs([]string{"-Interactive"}),
44+
WithBuildEntrypointExecCommandArgs([]string{"-Command"}),
45+
},
46+
&BuildEntrypoint{
47+
Entrypoint: []string{"pwsh", "-NoProfile"},
48+
ExecCommandArgs: []string{"-Command"},
49+
ExitOnErrorArgs: []string{},
50+
InteractiveArgs: []string{"-Interactive"},
51+
},
52+
},
53+
{
54+
"Elvish",
55+
[]BuildEntrypointOpt{
56+
WithBuildEntrypointEntrypoint([]string{"elvish", "-norc"}),
57+
WithBuildEntrypointInteractiveArgs([]string{"-i"}),
58+
WithBuildEntrypointExecCommandArgs([]string{"-c"}),
59+
},
60+
&BuildEntrypoint{
61+
Entrypoint: []string{"elvish", "-norc"},
62+
ExecCommandArgs: []string{"-c"},
63+
ExitOnErrorArgs: []string{},
64+
InteractiveArgs: []string{"-i"},
65+
},
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.description, func(t *testing.T) {
71+
actualBec := NewBuildEntrypoint(tt.opts...)
72+
assert.Equal(t, tt.expected, actualBec)
73+
})
74+
}
75+
}

src/core/build_target.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ type BuildTarget struct {
134134
OptionalOutputs []string `name:"optional_outs"`
135135
// Optional labels applied to this rule. Used for including/excluding rules.
136136
Labels []string
137-
// Shell command to run.
137+
// Build Entrypoint configuration.
138+
BuildEntryPoint *BuildEntrypoint `name:"build_entry_point" hide:"filegroup"`
139+
// Shell command to run, this is passed as the last argument to the Binary.
138140
Command string `name:"cmd" hide:"filegroup"`
139141
// Per-configuration shell commands to run.
140142
Commands map[string]string `name:"cmd" hide:"filegroup"`
@@ -384,6 +386,7 @@ func NewBuildTarget(label BuildLabel) *BuildTarget {
384386
state: int32(Inactive),
385387
BuildingDescription: DefaultBuildingDescription,
386388
finishedBuilding: make(chan struct{}),
389+
BuildEntryPoint: NewBuildEntrypoint(),
387390
}
388391
}
389392

src/output/shell_output.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,16 @@ func printTempDirs(state *core.BuildState, duration time.Duration, shell, shellR
443443
fmt.Printf(" Expanded: %s\n", os.Expand(cmd, env.ReplaceEnvironment))
444444
} else {
445445
fmt.Printf("\n")
446-
argv := []string{"bash", "--noprofile", "--norc", "-o", "pipefail"}
446+
buildArgvOpts := []core.BuildArgvOpt{target.BuildEntryPoint.WithBuildArgvInteractive()}
447447
if shellRun {
448-
argv = append(argv, "-c", cmd)
448+
buildArgvOpts = append(buildArgvOpts, target.BuildEntryPoint.WithBuildArgvCommand(cmd))
449449
}
450-
log.Debug("Full command: %s", strings.Join(argv, " "))
450+
argv, err := target.BuildEntryPoint.BuildArgv(state, target, buildArgvOpts...)
451+
if err != nil {
452+
log.Errorf("Could not build shell args: %s", err)
453+
}
454+
455+
log.Debug("Full command(shellRun: %v): %#v", shellRun, argv)
451456
cmd := state.ProcessExecutor.ExecCommand(process.NewSandboxConfig(shouldSandbox, shouldSandbox), false, argv[0], argv[1:]...)
452457
cmd.Dir = dir
453458
cmd.Env = append(cmd.Env, env.ToSlice()...)

src/parse/asp/targets.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ const (
7272
fileContentArgIdx
7373
subrepoArgIdx
7474
noTestCoverageArgIdx
75+
buildEntryPointArgIdx
76+
buildEntryPointExitOnErrorArgsArgIdx
77+
buildEntryPointInteractiveArgsArgIdx
78+
buildEntryPointExecCommandArgsArgIdx
7579
)
7680

7781
// createTarget creates a new build target as part of build_rule().
@@ -140,6 +144,8 @@ func createTarget(s *scope, args []pyObject) *core.BuildTarget {
140144
target.AddLabel("remote")
141145
}
142146
target.Command, target.Commands = decodeCommands(s, args[cmdBuildRuleArgIdx])
147+
target.BuildEntryPoint = decodeBuildEntrypointFromArgs(s, args)
148+
143149
if test {
144150
target.Test = new(core.TestFields)
145151

@@ -252,6 +258,37 @@ func decodeCommands(s *scope, obj pyObject) (string, map[string]string) {
252258
return "", m
253259
}
254260

261+
// decodeBuildEntrypointFromArgs takes a Python object and returns it as a BuildEntrypoint.
262+
func decodeBuildEntrypointFromArgs(s *scope, args []pyObject) *core.BuildEntrypoint {
263+
buildEntrypointOpts := []core.BuildEntrypointOpt{}
264+
265+
if obj := args[buildEntryPointArgIdx]; obj != nil && obj != None {
266+
buildEntrypointOpts = append(buildEntrypointOpts,
267+
core.WithBuildEntrypointEntrypoint(asStringList(s, mustList(obj), "build_entry_point")),
268+
)
269+
}
270+
271+
if obj := args[buildEntryPointExecCommandArgsArgIdx]; obj != nil && obj != None {
272+
buildEntrypointOpts = append(buildEntrypointOpts,
273+
core.WithBuildEntrypointExecCommandArgs(asStringList(s, mustList(obj), "build_entry_point_exec_command_args")),
274+
)
275+
}
276+
277+
if obj := args[buildEntryPointExitOnErrorArgsArgIdx]; obj != nil && obj != None {
278+
buildEntrypointOpts = append(buildEntrypointOpts,
279+
core.WithBuildEntrypointExitOnErrorArgs(asStringList(s, mustList(obj), "build_entry_point_exit_on_error_args")),
280+
)
281+
}
282+
283+
if obj := args[buildEntryPointInteractiveArgsArgIdx]; obj != nil && obj != None {
284+
buildEntrypointOpts = append(buildEntrypointOpts,
285+
core.WithBuildEntrypointInteractiveArgs(asStringList(s, mustList(obj), "build_entry_point_interactive_args")),
286+
)
287+
}
288+
289+
return core.NewBuildEntrypoint(buildEntrypointOpts...)
290+
}
291+
255292
// populateTarget sets the assorted attributes on a build target.
256293
func populateTarget(s *scope, t *core.BuildTarget, args []pyObject) {
257294
if t.IsRemoteFile {

0 commit comments

Comments
 (0)