@@ -18,6 +18,8 @@ package build
1818
1919import (
2020 "context"
21+ "errors"
22+ "fmt"
2123 "io"
2224 "os"
2325 "path/filepath"
@@ -29,6 +31,7 @@ import (
2931 "github.com/moby/buildkit/cmd/buildctl/build"
3032 "github.com/moby/buildkit/session"
3133 "github.com/sirupsen/logrus"
34+ "github.com/tonistiigi/go-csvvalue"
3235 "google.golang.org/grpc"
3336)
3437
@@ -53,7 +56,7 @@ func Build(ctx context.Context, opts *BOpts) error {
5356 }
5457 defer buildkit .Close ()
5558
56- exports , err := build . ParseOutput (opts .Outputs )
59+ exports , err := parseOutput (opts .Outputs )
5760 if err != nil {
5861 return err
5962 }
@@ -85,10 +88,21 @@ func Build(ctx context.Context, opts *BOpts) error {
8588
8689 exportsWithOutput := []client.ExportEntry {}
8790 for _ , export := range exports {
88- export .Output = func (map [string ]string ) (io.WriteCloser , error ) {
89- return wf , nil
91+ switch export .Type {
92+ case client .ExporterLocal :
93+ localDest := filepath .Join (GlobalExportPath , opts .BuildID , "local" )
94+ os .MkdirAll (localDest , 0o755 )
95+ if export .OutputDir == "" {
96+ export .OutputDir = localDest
97+ }
98+ export .Attrs ["dest" ] = localDest
99+ default : // oci, tar
100+ export .Output = func (map [string ]string ) (io.WriteCloser , error ) {
101+ return wf , nil
102+ }
103+ export .Attrs ["output" ] = filepath .Join (GlobalExportPath , opts .BuildID , "out.tar" )
90104 }
91- export . Attrs [ "output" ] = filepath . Join ( GlobalExportPath , opts . BuildID , "out.tar" )
105+
92106 if _ , ok := export .Attrs ["name" ]; ! ok {
93107 export .Attrs ["name" ] = opts .Tag
94108 }
@@ -183,3 +197,68 @@ func (w *wrappedWriteCloser) Close() error {
183197 }
184198 return nil
185199}
200+
201+ // parseOutput parses CSV output strings and returns ExportEntry slice.
202+ // It validates the output types and allows type=local without dest field.
203+ // Supported types: oci, tar, local
204+ func parseOutput (outputs []string ) ([]client.ExportEntry , error ) {
205+ var entries []client.ExportEntry
206+
207+ for _ , output := range outputs {
208+ entry , err := parseOutputCSV (output )
209+ if err != nil {
210+ return nil , err
211+ }
212+ entries = append (entries , entry )
213+ }
214+
215+ return entries , nil
216+ }
217+
218+ // parseOutputCSV parses a single CSV output string into an ExportEntry
219+ func parseOutputCSV (output string ) (client.ExportEntry , error ) {
220+ entry := client.ExportEntry {
221+ Attrs : make (map [string ]string ),
222+ }
223+
224+ // Parse CSV fields
225+ fields , err := csvvalue .Fields (output , nil )
226+ if err != nil {
227+ return entry , fmt .Errorf ("failed to parse CSV: %w" , err )
228+ }
229+
230+ // Process each field
231+ for _ , field := range fields {
232+ key , value , ok := strings .Cut (field , "=" )
233+ if ! ok {
234+ return entry , fmt .Errorf ("invalid field format: %s (expected key=value)" , field )
235+ }
236+
237+ key = strings .ToLower (strings .TrimSpace (key ))
238+ value = strings .TrimSpace (value )
239+
240+ switch key {
241+ case "type" :
242+ entry .Type = value
243+ default :
244+ entry .Attrs [key ] = value
245+ }
246+ }
247+
248+ // Validate type is provided
249+ if entry .Type == "" {
250+ return entry , errors .New ("output type is required (type=<type>)" )
251+ }
252+
253+ // Validate supported types
254+ switch entry .Type {
255+ case "oci" , "tar" , "local" :
256+ // These are the supported types
257+ default :
258+ return entry , fmt .Errorf ("unsupported output type: %s (supported: oci, tar, local)" , entry .Type )
259+ }
260+
261+ // No path validation - just return the parsed entry
262+
263+ return entry , nil
264+ }
0 commit comments