package buildstep import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" ) // OutputVerb is the verb of a build seat that answers a step's answer kept as an object (design 47 G A-254, A-290). const OutputVerb = "output" // AskVerb asks a seat verb as the runtime calls it (stdio.Ask): the key `seat:.[@]` and the body in, the // verb's answer out. A verb marked chunks is asked by the plain call, and the runtime hands the whole file in `file` (A-249). type AskVerb func(key string, body any) (json.RawMessage, error) // ReadOutput is the answer a build seat's step-ended carries (design 47 G A-254): its inline `output`, else the object // `output-object` names, read through the verb `output` of the seat of language on the node that ran the ask. Every asker of a // run reads the answer this way, whichever build seat ran it; none reads the seat's step state. func (e StepEnded) ReadOutput(language string, ask AskVerb) ([]byte, error) { if e.OutputObject == "" { return []byte(e.Output), nil } return readObject(language, e.Node, e.Ask, e.OutputObject, ask) } // Whole is the step with its answer whole (design 47 G A-290): a step whose key names its answer as the object // `output-object`, its value being over what the steps state holds, has it read through the verb `output` of the seat of // language on the step's node, and put back where the key holds the answer itself: a run's (run true) as its run answer's // output, any other's as its result. A step whose key holds its answer is answered as it is. Every reader of a step's answer // reads it so, the checker and the builder alike (no step reads passed in one place and failed in another). func (s Step) Whole(language string, run bool, ask AskVerb) (Step, error) { if s.OutputObject == "" { return s, nil } body, err := readObject(language, s.Node, s.ID, s.OutputObject, ask) if err != nil { return s, err } if !run { if !json.Valid(body) { return s, fmt.Errorf("buildstep: the answer %s of the ask %s is not JSON", s.OutputObject, s.ID) } s.Result, s.OutputObject = json.RawMessage(body), "" return s, nil } var answer RunAnswer if len(s.Result) > 0 { if err := json.Unmarshal(s.Result, &answer); err != nil { return s, fmt.Errorf("buildstep: the run answer of the ask %s is not one: %w", s.ID, err) } } answer.Output = json.RawMessage(body) raw, err := json.Marshal(answer) if err != nil { return s, fmt.Errorf("buildstep: the run answer of the ask %s with its output %s: %w", s.ID, s.OutputObject, err) } s.Result, s.OutputObject = raw, "" return s, nil } // readObject reads the object a build seat's holder on node keeps of the ask askID through the seat's verb `output`. func readObject(language, node, askID, object string, ask AskVerb) ([]byte, error) { seat, err := BuildSeat(language) if err != nil { return nil, err } if ask == nil { return nil, errors.New("buildstep: the answer is in the output object " + object + ", which this reader has no verb to read") } var call [16]byte if _, err := rand.Read(call[:]); err != nil { return nil, err } raw, err := ask("seat:"+seat+"."+OutputVerb+"@"+node, map[string]any{"ask": askID, "call": hex.EncodeToString(call[:])}) if err != nil { return nil, fmt.Errorf("buildstep: asking %s for the answer %s of the ask %s: %w", seat, object, askID, err) } var got struct { Ask string `json:"ask"` File string `json:"file"` Chunks int `json:"chunks"` } if err := json.Unmarshal(raw, &got); err != nil { return nil, fmt.Errorf("buildstep: the verb output of %s answered no answer for %s: %w", seat, askID, err) } if got.Ask != askID { return nil, fmt.Errorf("buildstep: the verb output of %s answered the ask %q, asked %s", seat, got.Ask, askID) } if got.File == "" { return nil, fmt.Errorf("buildstep: %s on %s keeps no answer of the ask %s", seat, node, askID) } body, err := base64.StdEncoding.DecodeString(got.File) if err != nil { return nil, fmt.Errorf("buildstep: the answer of %s in no base64: %w", askID, err) } return body, nil }