Skip to content

Commit e24545f

Browse files
authored
cmd/boot: harden engine execution (#358)
Assisted-by: GPT-5.5
1 parent 8cf2eea commit e24545f

34 files changed

Lines changed: 1717 additions & 699 deletions

cmd/boot/internal/README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ contracts used by the built-in modules.
99
`main.go` builds an `internal.Engine` with:
1010

1111
- a `Runtime`, which stores recipe root, home directory, environment access,
12-
stdio, and whether the current run is interactive;
12+
runtime environment overrides, stdio, color preference, and whether the current
13+
run is interactive;
1314
- an entrypoint, normally `BOOT.star`;
1415
- a list of modules, each exposed as a Starlark module.
1516

1617
`Engine.Load` executes the entrypoint with predeclared globals:
1718

1819
- `task(...)` registers a task;
1920
- `fail(message)` stops recipe evaluation;
21+
- `host()` returns host/runtime metadata for top-level branching;
2022
- each Go module appears under its module name, such as `fs` or `pkg`.
2123

2224
Top-level Starlark should only register tasks and choose machine profiles. Host
@@ -25,8 +27,8 @@ mutation belongs in task functions through module actions.
2527
## Tasks and Actions
2628

2729
A task is a named Starlark callable. When the engine runs a task, it attaches
28-
the task to the Starlark thread with `SetTask`. Module functions validate
29-
`InTask(thread)` and call `AddAction(thread, Action{...})`.
30+
the task to the Starlark thread with `SetTask`. Module functions call
31+
`RequireTask(thread, b)` and then `AddAction(thread, Action{...})`.
3032

3133
An `Action` has:
3234

@@ -59,16 +61,18 @@ that emits one idempotent action over a general-purpose command wrapper.
5961

6062
Module function checklist:
6163

62-
- Require task context for functions that emit actions.
64+
- Require task context for functions that emit actions with `boot.RequireTask`.
6365
- Parse arguments with `starlark.UnpackArgs`.
6466
- Resolve recipe inputs with `Runtime.ResolveSource`.
6567
- Resolve host targets with `Runtime.ResolveTarget`.
66-
- Use `Runtime.ExpandHome` or `Runtime.Hostname` rather than duplicating that
67-
logic.
68+
- Use `Runtime.ExpandHome`, `Runtime.Hostname`, `Runtime.EnvValue`, and
69+
`Runtime.SetEnv` rather than duplicating that logic.
6870
- Do not mutate the host while registering actions.
6971
- In dry-run, perform enough checks to decide skip/change but do not write.
70-
- Include command output in returned errors; use `boot.CommandError` when it
71-
fits.
72+
- Include command output in returned errors; prefer `boot.RunCommand`,
73+
`boot.RunCmd`, `boot.CommandOutput`, or `boot.CommandError`.
74+
- Use `boot.Output` and `boot.BulletList` for user-visible check details.
75+
- Validate Starlark file modes with `boot.FileMode`.
7276
- Keep successful JSON or textual output minimal; noisy reporting belongs in
7377
explicit check modules.
7478

@@ -93,6 +97,10 @@ Use `--json` when another program needs stable output. JSON runs intentionally
9397
use a simpler sequential execution path so action results are ordered and easy
9498
to consume.
9599

100+
Task selection is strict: if `-only`, `-skip`, or `-tag` leaves a selected task
101+
without one of its declared dependencies, selection fails instead of silently
102+
ignoring the missing dependency.
103+
96104
When debugging command execution, prefer fake commands in a temporary `PATH`
97105
inside tests. See the `packages`, `systemd`, and `rescue` tests for examples.
98106

cmd/boot/internal/command.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,36 @@ func RunCommand(ctx context.Context, dir string, argv ...string) error {
1818
if dir != "" {
1919
cmd.Dir = dir
2020
}
21+
return RunCmd(cmd)
22+
}
23+
24+
// RunCmd runs cmd and includes combined output in returned errors.
25+
func RunCmd(cmd *exec.Cmd) error {
2126
var buf bytes.Buffer
2227
cmd.Stdout = &buf
2328
cmd.Stderr = &buf
2429
if err := cmd.Run(); err != nil {
25-
return CommandError(argv, buf.Bytes(), err)
30+
return CommandError(cmd.Args, buf.Bytes(), err)
2631
}
2732
return nil
2833
}
2934

35+
// CommandOutput runs argv and returns stdout, including combined output in returned errors.
36+
func CommandOutput(ctx context.Context, dir string, argv ...string) ([]byte, error) {
37+
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
38+
if dir != "" {
39+
cmd.Dir = dir
40+
}
41+
var stderr bytes.Buffer
42+
cmd.Stderr = &stderr
43+
out, err := cmd.Output()
44+
if err != nil {
45+
combined := append(append([]byte{}, out...), stderr.Bytes()...)
46+
return nil, CommandError(cmd.Args, combined, err)
47+
}
48+
return out, nil
49+
}
50+
3051
// CommandError formats a command failure with trimmed command output.
3152
func CommandError(argv []string, out []byte, err error) error {
3253
msg := strings.TrimSpace(string(out))

cmd/boot/internal/consent/consent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ type impl struct {
3737
}
3838

3939
func (m *impl) require(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
40-
if !boot.InTask(thread) {
41-
return nil, fmt.Errorf("%s: can only be called from a task", b.Name())
40+
if err := boot.RequireTask(thread, b); err != nil {
41+
return nil, err
4242
}
4343

4444
var (

0 commit comments

Comments
 (0)