Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 59 additions & 12 deletions rules/builtins.build_defs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,65 @@


# Do not change the order of arguments to this function without updating the iota in targets.go to match it.
def build_rule(name:str, cmd:str|dict='', test_cmd:str|dict='', debug_cmd:str='', srcs:list|dict=None, data:list|dict=None,
debug_data:list|dict=None, outs:list|dict=None, deps:list=None, exported_deps:list=None, secrets:list|dict=None,
tools:str|list|dict=None, test_tools:str|list|dict=None, debug_tools:str|list|dict=None, labels:list=None,
visibility:list=CONFIG.DEFAULT_VISIBILITY, hashes:list=None, binary:bool=False, test:bool=False,
test_only:bool=CONFIG.DEFAULT_TESTONLY, building_description:str=None, needs_transitive_deps:bool=False,
output_is_complete:bool=False, sandbox:bool=CONFIG.BUILD_SANDBOX, test_sandbox:bool=CONFIG.TEST_SANDBOX,
no_test_output:bool=False, flaky:bool|int=0, build_timeout:int|str=0, test_timeout:int|str=0, pre_build:function=None,
post_build:function=None, requires:list=None, provides:dict=None, licences:list=CONFIG.DEFAULT_LICENCES,
test_outputs:list=None, system_srcs:list=None, stamp:bool=False, tag:str='', optional_outs:list=None, progress:bool=False,
size:str=None, _urls:list=None, internal_deps:list=None, pass_env:list=None, local:bool=False, output_dirs:list=[],
exit_on_error:bool=CONFIG.EXIT_ON_ERROR, entry_points:dict={}, env:dict={}, _file_content:str=None,
_subrepo:bool=False, no_test_coverage:bool=False):
def build_rule(
name:str,
cmd:str|dict="",
test_cmd:str|dict="",
debug_cmd:str="",
srcs:list|dict=None,
data:list|dict=None,
debug_data:list|dict=None,
outs:list|dict=None,
deps:list=None,
exported_deps:list=None,
secrets:list|dict=None,
tools:str|list|dict=None,
test_tools:str|list|dict=None,
debug_tools:str|list|dict=None,
labels:list=None,
visibility:list=CONFIG.DEFAULT_VISIBILITY,
hashes:list=None,
binary:bool=False,
test:bool=False,
test_only:bool=CONFIG.DEFAULT_TESTONLY,
building_description:str=None,
needs_transitive_deps:bool=False,
output_is_complete:bool=False,
sandbox:bool=CONFIG.BUILD_SANDBOX,
test_sandbox:bool=CONFIG.TEST_SANDBOX,
no_test_output:bool=False,
flaky:bool|int=0,
build_timeout:int|str=0,
test_timeout:int|str=0,
pre_build:function=None,
post_build:function=None,
requires:list=None,
provides:dict=None,
licences:list=CONFIG.DEFAULT_LICENCES,
test_outputs:list=None,
system_srcs:list=None,
stamp:bool=False,
tag:str="",
optional_outs:list=None,
progress:bool=False,
size:str=None,
_urls:list=None,
internal_deps:list=None,
pass_env:list=None,
local:bool=False,
output_dirs:list=[],
exit_on_error:bool=CONFIG.EXIT_ON_ERROR,
entry_points:dict={},
env:dict={},
_file_content:str=None,
_subrepo:bool=False,
no_test_coverage:bool=False,
# This matches the default `BuildEntrypoint` is defined in
#`src/core/build_entrypoint.go`.
build_entry_point:list=None,
build_entry_point_exit_on_error_args:list=None,
build_entry_point_interactive_args:list=None,
build_entry_point_exec_command_args:list=None):
pass

def chr(i:int) -> str:
Expand Down
19 changes: 18 additions & 1 deletion src/build/build_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package build

import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -515,12 +516,28 @@ func runBuildCommand(state *core.BuildState, target *core.BuildTarget, command s
if target.IsTextFile {
return nil, buildTextFile(state, target)
}

env := core.StampedBuildEnvironment(state, target, inputHash, filepath.Join(core.RepoRoot, target.TmpDir()), target.Stamp).ToSlice()
log.Debug("Building target %s\nENVIRONMENT:\n%s\n%s", target.Label, env, command)
out, combined, err := state.ProcessExecutor.ExecWithTimeoutShell(target, target.TmpDir(), env, target.BuildTimeout, state.ShowAllOutput, false, process.NewSandboxConfig(target.Sandbox, target.Sandbox), command)

buildArgvOpts := []core.BuildArgvOpt{target.BuildEntryPoint.WithBuildArgvCommand(command)}
if target.ShouldExitOnError() {
buildArgvOpts = append(buildArgvOpts, target.BuildEntryPoint.WithBuildArgvExitOnError())
}
argv, err := target.BuildEntryPoint.BuildArgv(state, target, buildArgvOpts...)
if err != nil {
return nil, err
}
out, combined, err := state.ProcessExecutor.ExecWithTimeout(
context.Background(),
target, target.TmpDir(), env, target.BuildTimeout, state.ShowAllOutput,
false, false, false, process.NewSandboxConfig(target.Sandbox, target.Sandbox),
argv,
)
if err != nil {
return nil, fmt.Errorf("Error building target %s: %s\n%s", target.Label, err, combined)
}

return out, nil
}

Expand Down
1 change: 1 addition & 0 deletions src/build/incrementality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var KnownFields = map[string]bool{
"IsTextFile": true,
"FileContent": true,
"IsRemoteFile": true,
"BuildEntryPoint": true,
"Command": true,
"Commands": true,
"NeedsTransitiveDependencies": true,
Expand Down
96 changes: 96 additions & 0 deletions src/core/build_entrypoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package core

type BuildEntrypoint struct {
Entrypoint []string
ExecCommandArgs []string
ExitOnErrorArgs []string
InteractiveArgs []string
}

type BuildEntrypointOpt func(*BuildEntrypoint)

func WithBuildEntrypointEntrypoint(entrypoint []string) BuildEntrypointOpt {
return func(be *BuildEntrypoint) {
be.Entrypoint = entrypoint
}
}

func WithBuildEntrypointExitOnErrorArgs(args []string) BuildEntrypointOpt {
return func(be *BuildEntrypoint) {
be.ExitOnErrorArgs = args
}
}

func WithBuildEntrypointExecCommandArgs(args []string) BuildEntrypointOpt {
return func(be *BuildEntrypoint) {
be.ExecCommandArgs = args
}
}

func WithBuildEntrypointInteractiveArgs(args []string) BuildEntrypointOpt {
return func(be *BuildEntrypoint) {
be.InteractiveArgs = args
}
}

func NewBuildEntrypoint(opts ...BuildEntrypointOpt) *BuildEntrypoint {
be := &BuildEntrypoint{
Entrypoint: []string{},
ExecCommandArgs: []string{},
ExitOnErrorArgs: []string{},
InteractiveArgs: []string{},
}

for _, opt := range opts {
opt(be)
}

// Default to Bash if Entrypoint not set.
if len(be.Entrypoint) < 1 {
be.Entrypoint = []string{"bash", "--noprofile", "--norc", "-u", "-o", "pipefail"}
be.ExecCommandArgs = []string{"-c"}
be.ExitOnErrorArgs = []string{"-e"}
be.InteractiveArgs = []string{}
}

return be
}

type BuildArgv struct{ Argv []string }
type BuildArgvOpt func(*BuildArgv)

func (be *BuildEntrypoint) WithBuildArgvExitOnError() BuildArgvOpt {
return func(ba *BuildArgv) {
ba.Argv = append(ba.Argv, be.ExitOnErrorArgs...)
}
}

func (be *BuildEntrypoint) WithBuildArgvInteractive() BuildArgvOpt {
return func(ba *BuildArgv) {
log.Debugf("pre interactive argv: %#v", ba.Argv)
ba.Argv = append(ba.Argv, be.InteractiveArgs...)

log.Debugf("post interactive argv: %#v", ba.Argv)
}
}

func (be *BuildEntrypoint) WithBuildArgvCommand(command string) BuildArgvOpt {
return func(ba *BuildArgv) {
ba.Argv = append(ba.Argv, append(be.ExecCommandArgs, command)...)
}
}

func (be *BuildEntrypoint) BuildArgv(buildState *BuildState, target *BuildTarget, opts ...BuildArgvOpt) ([]string, error) {
argv := &BuildArgv{Argv: be.Entrypoint}
for _, opt := range opts {
opt(argv)
}

newArg0, err := ReplaceSequences(buildState, target, argv.Argv[0])
if err != nil {
return nil, err
}
argv.Argv[0] = newArg0

return argv.Argv, nil
}
75 changes: 75 additions & 0 deletions src/core/build_entrypoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package core

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewBuildEntrypoint(t *testing.T) {
var tests = []struct {
description string
opts []BuildEntrypointOpt
expected *BuildEntrypoint
}{
{
"DefaultConfigIsBash",
nil,
&BuildEntrypoint{
Entrypoint: []string{"bash", "--noprofile", "--norc", "-u", "-o", "pipefail"},
ExecCommandArgs: []string{"-c"},
ExitOnErrorArgs: []string{"-e"},
InteractiveArgs: []string{},
},
},
{
"NuShell",
[]BuildEntrypointOpt{
WithBuildEntrypointEntrypoint([]string{"nu", "--no-config-file", "--no-history"}),
WithBuildEntrypointInteractiveArgs([]string{"--execute", "$env.config.show_banner = false"}),
WithBuildEntrypointExecCommandArgs([]string{"--commands"}),
},
&BuildEntrypoint{
Entrypoint: []string{"nu", "--no-config-file", "--no-history"},
ExecCommandArgs: []string{"--commands"},
ExitOnErrorArgs: []string{},
InteractiveArgs: []string{"--execute", "$env.config.show_banner = false"},
},
},
{
"Powershell",
[]BuildEntrypointOpt{
WithBuildEntrypointEntrypoint([]string{"pwsh", "-NoProfile"}),
WithBuildEntrypointInteractiveArgs([]string{"-Interactive"}),
WithBuildEntrypointExecCommandArgs([]string{"-Command"}),
},
&BuildEntrypoint{
Entrypoint: []string{"pwsh", "-NoProfile"},
ExecCommandArgs: []string{"-Command"},
ExitOnErrorArgs: []string{},
InteractiveArgs: []string{"-Interactive"},
},
},
{
"Elvish",
[]BuildEntrypointOpt{
WithBuildEntrypointEntrypoint([]string{"elvish", "-norc"}),
WithBuildEntrypointInteractiveArgs([]string{"-i"}),
WithBuildEntrypointExecCommandArgs([]string{"-c"}),
},
&BuildEntrypoint{
Entrypoint: []string{"elvish", "-norc"},
ExecCommandArgs: []string{"-c"},
ExitOnErrorArgs: []string{},
InteractiveArgs: []string{"-i"},
},
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
actualBec := NewBuildEntrypoint(tt.opts...)
assert.Equal(t, tt.expected, actualBec)
})
}
}
5 changes: 4 additions & 1 deletion src/core/build_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ type BuildTarget struct {
OptionalOutputs []string `name:"optional_outs"`
// Optional labels applied to this rule. Used for including/excluding rules.
Labels []string
// Shell command to run.
// Build Entrypoint configuration.
BuildEntryPoint *BuildEntrypoint `name:"build_entry_point" hide:"filegroup"`
// Shell command to run, this is passed as the last argument to the Binary.
Command string `name:"cmd" hide:"filegroup"`
// Per-configuration shell commands to run.
Commands map[string]string `name:"cmd" hide:"filegroup"`
Expand Down Expand Up @@ -384,6 +386,7 @@ func NewBuildTarget(label BuildLabel) *BuildTarget {
state: int32(Inactive),
BuildingDescription: DefaultBuildingDescription,
finishedBuilding: make(chan struct{}),
BuildEntryPoint: NewBuildEntrypoint(),
}
}

Expand Down
11 changes: 8 additions & 3 deletions src/output/shell_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,16 @@ func printTempDirs(state *core.BuildState, duration time.Duration, shell, shellR
fmt.Printf(" Expanded: %s\n", os.Expand(cmd, env.ReplaceEnvironment))
} else {
fmt.Printf("\n")
argv := []string{"bash", "--noprofile", "--norc", "-o", "pipefail"}
buildArgvOpts := []core.BuildArgvOpt{target.BuildEntryPoint.WithBuildArgvInteractive()}
if shellRun {
argv = append(argv, "-c", cmd)
buildArgvOpts = append(buildArgvOpts, target.BuildEntryPoint.WithBuildArgvCommand(cmd))
}
log.Debug("Full command: %s", strings.Join(argv, " "))
argv, err := target.BuildEntryPoint.BuildArgv(state, target, buildArgvOpts...)
if err != nil {
log.Errorf("Could not build shell args: %s", err)
}

log.Debug("Full command(shellRun: %v): %#v", shellRun, argv)
cmd := state.ProcessExecutor.ExecCommand(process.NewSandboxConfig(shouldSandbox, shouldSandbox), false, argv[0], argv[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Env, env.ToSlice()...)
Expand Down
37 changes: 37 additions & 0 deletions src/parse/asp/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ const (
fileContentArgIdx
subrepoArgIdx
noTestCoverageArgIdx
buildEntryPointArgIdx
buildEntryPointExitOnErrorArgsArgIdx
buildEntryPointInteractiveArgsArgIdx
buildEntryPointExecCommandArgsArgIdx
)

// createTarget creates a new build target as part of build_rule().
Expand Down Expand Up @@ -140,6 +144,8 @@ func createTarget(s *scope, args []pyObject) *core.BuildTarget {
target.AddLabel("remote")
}
target.Command, target.Commands = decodeCommands(s, args[cmdBuildRuleArgIdx])
target.BuildEntryPoint = decodeBuildEntrypointFromArgs(s, args)

if test {
target.Test = new(core.TestFields)

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

// decodeBuildEntrypointFromArgs takes a Python object and returns it as a BuildEntrypoint.
func decodeBuildEntrypointFromArgs(s *scope, args []pyObject) *core.BuildEntrypoint {
buildEntrypointOpts := []core.BuildEntrypointOpt{}

if obj := args[buildEntryPointArgIdx]; obj != nil && obj != None {
buildEntrypointOpts = append(buildEntrypointOpts,
core.WithBuildEntrypointEntrypoint(asStringList(s, mustList(obj), "build_entry_point")),
)
}

if obj := args[buildEntryPointExecCommandArgsArgIdx]; obj != nil && obj != None {
buildEntrypointOpts = append(buildEntrypointOpts,
core.WithBuildEntrypointExecCommandArgs(asStringList(s, mustList(obj), "build_entry_point_exec_command_args")),
)
}

if obj := args[buildEntryPointExitOnErrorArgsArgIdx]; obj != nil && obj != None {
buildEntrypointOpts = append(buildEntrypointOpts,
core.WithBuildEntrypointExitOnErrorArgs(asStringList(s, mustList(obj), "build_entry_point_exit_on_error_args")),
)
}

if obj := args[buildEntryPointInteractiveArgsArgIdx]; obj != nil && obj != None {
buildEntrypointOpts = append(buildEntrypointOpts,
core.WithBuildEntrypointInteractiveArgs(asStringList(s, mustList(obj), "build_entry_point_interactive_args")),
)
}

return core.NewBuildEntrypoint(buildEntrypointOpts...)
}

// populateTarget sets the assorted attributes on a build target.
func populateTarget(s *scope, t *core.BuildTarget, args []pyObject) {
if t.IsRemoteFile {
Expand Down
Loading
Loading