@@ -18,6 +18,8 @@ package build
18
18
19
19
import (
20
20
"context"
21
+ "errors"
22
+ "fmt"
21
23
"io"
22
24
"os"
23
25
"path/filepath"
@@ -29,6 +31,7 @@ import (
29
31
"github.com/moby/buildkit/cmd/buildctl/build"
30
32
"github.com/moby/buildkit/session"
31
33
"github.com/sirupsen/logrus"
34
+ "github.com/tonistiigi/go-csvvalue"
32
35
"google.golang.org/grpc"
33
36
)
34
37
@@ -53,7 +56,7 @@ func Build(ctx context.Context, opts *BOpts) error {
53
56
}
54
57
defer buildkit .Close ()
55
58
56
- exports , err := build . ParseOutput (opts .Outputs )
59
+ exports , err := parseOutput (opts .Outputs )
57
60
if err != nil {
58
61
return err
59
62
}
@@ -85,10 +88,21 @@ func Build(ctx context.Context, opts *BOpts) error {
85
88
86
89
exportsWithOutput := []client.ExportEntry {}
87
90
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" )
90
104
}
91
- export . Attrs [ "output" ] = filepath . Join ( GlobalExportPath , opts . BuildID , "out.tar" )
105
+
92
106
if _ , ok := export .Attrs ["name" ]; ! ok {
93
107
export .Attrs ["name" ] = opts .Tag
94
108
}
@@ -183,3 +197,68 @@ func (w *wrappedWriteCloser) Close() error {
183
197
}
184
198
return nil
185
199
}
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