Skip to content

Commit 01db27a

Browse files
committed
[Windows] nerdctl build - Separate TTY and tar output
- Write buildctl output to terminal and tarball to file Signed-off-by: Christine Murimi <mor.devx@outlook.com>
1 parent 49a19ed commit 01db27a

File tree

1 file changed

+97
-29
lines changed

1 file changed

+97
-29
lines changed

pkg/cmd/builder/build.go

+97-29
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"os"
2626
"os/exec"
2727
"path/filepath"
28+
"runtime"
2829
"strconv"
2930
"strings"
3031

@@ -61,18 +62,22 @@ func (p platformParser) DefaultSpec() platforms.Platform {
6162
}
6263

6364
func Build(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) error {
64-
buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(ctx, client, options)
65+
buildCtlArgs, err := generateBuildctlArgs(ctx, client, options)
6566
if err != nil {
6667
return err
6768
}
68-
if cleanup != nil {
69-
defer cleanup()
69+
if buildCtlArgs.Cleanup != nil {
70+
defer buildCtlArgs.Cleanup()
7071
}
7172

73+
buildctlBinary := buildCtlArgs.BuildctlBinary
74+
buildctlArgs := buildCtlArgs.BuildctlArgs
75+
7276
log.L.Debugf("running %s %v", buildctlBinary, buildctlArgs)
7377
buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
7478
buildctlCmd.Env = os.Environ()
7579

80+
needsLoading := buildCtlArgs.NeedsLoading
7681
var buildctlStdout io.Reader
7782
if needsLoading {
7883
buildctlStdout, err = buildctlCmd.StdoutPipe()
@@ -95,6 +100,26 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
95100
if err != nil {
96101
return err
97102
}
103+
104+
if buildCtlArgs.DestFile == "" {
105+
log.L.Debug("no tar file specified")
106+
} else {
107+
// Separate TTY (image loading) buildctl output and tarball output
108+
// Write buildctl output to stdout
109+
if _, err := io.Copy(os.Stdout, buildctlStdout); err != nil {
110+
return err
111+
}
112+
113+
// Open the tar file
114+
reader, err := os.Open(buildCtlArgs.DestFile)
115+
if err != nil {
116+
return fmt.Errorf("failed to open tar file: %v", err)
117+
}
118+
defer reader.Close()
119+
buildctlStdout = reader
120+
}
121+
122+
// Load the image into the containerd image store
98123
if err = loadImage(ctx, buildctlStdout, options.GOptions.Namespace, options.GOptions.Address, options.GOptions.Snapshotter, options.Stdout, platMC, options.Quiet); err != nil {
99124
return err
100125
}
@@ -105,7 +130,7 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
105130
}
106131

107132
if options.IidFile != "" {
108-
id, err := getDigestFromMetaFile(metaFile)
133+
id, err := getDigestFromMetaFile(buildCtlArgs.MetaFile)
109134
if err != nil {
110135
return err
111136
}
@@ -114,6 +139,7 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
114139
}
115140
}
116141

142+
tags := buildCtlArgs.Tags
117143
if len(tags) > 1 {
118144
log.L.Debug("Found more than 1 tag")
119145
imageService := client.ImageService()
@@ -160,7 +186,11 @@ func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotte
160186
client.Close()
161187
}()
162188
r := &readCounter{Reader: in}
163-
imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC))
189+
imgs, err := client.Import(ctx, r,
190+
containerd.WithDigestRef(archive.DigestTranslator(snapshotter)),
191+
containerd.WithSkipDigestRef(func(name string) bool { return name != "" }),
192+
containerd.WithImportPlatform(platMC),
193+
)
164194
if err != nil {
165195
if r.N == 0 {
166196
// Avoid confusing "unrecognized image format"
@@ -192,23 +222,40 @@ func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotte
192222
return nil
193223
}
194224

195-
func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (buildCtlBinary string,
196-
buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) {
225+
type BuildctlArgsResult struct {
226+
BuildctlArgs []string
227+
BuildctlBinary string
228+
Cleanup func()
229+
DestFile string
230+
MetaFile string
231+
NeedsLoading bool // Specifies whether the image needs to be loaded into the containerd image store
232+
Tags []string
233+
}
197234

235+
func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (result BuildctlArgsResult, err error) {
198236
buildctlBinary, err := buildkitutil.BuildctlBinary()
199237
if err != nil {
200-
return "", nil, false, "", nil, nil, err
238+
return result, err
201239
}
240+
result.BuildctlBinary = buildctlBinary
241+
242+
// FIXME: Find a better path
243+
// Set the default destination file
244+
defaultDestFile, err := filepath.Abs("output.tar")
245+
if err != nil {
246+
return result, fmt.Errorf("failed to set the default destination file path: %v", err)
247+
}
248+
var defaultDest string
202249

203250
output := options.Output
204251
if output == "" {
205252
info, err := client.Server(ctx)
206253
if err != nil {
207-
return "", nil, false, "", nil, nil, err
254+
return result, err
208255
}
209256
sharable, err := isImageSharable(options.BuildKitHost, options.GOptions.Namespace, info.UUID, options.GOptions.Snapshotter, options.Platform)
210257
if err != nil {
211-
return "", nil, false, "", nil, nil, err
258+
return result, err
212259
}
213260
if sharable {
214261
output = "type=image,unpack=true" // ensure the target stage is unlazied (needed for any snapshotters)
@@ -219,42 +266,61 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
219266
// TODO: consider using type=oci for single-options.Platform build too
220267
output = "type=oci"
221268
}
222-
needsLoading = true
223269
}
224270
} else {
225271
if !strings.Contains(output, "type=") {
226272
// should accept --output <DIR> as an alias of --output
227273
// type=local,dest=<DIR>
228274
output = fmt.Sprintf("type=local,dest=%s", output)
229275
}
230-
if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") {
231-
if !strings.Contains(output, "dest=") {
232-
needsLoading = true
276+
}
277+
278+
// The `buildctl build` command sends both tty logs and tar (binary data) to stdout.
279+
// Windows terminals can't distinguish these, causing "archive/tar: invalid tar header" error.
280+
// To prevent this, we direct tar output to a file, ensuring separate handling of tty and tar data.
281+
// A default tar file is set (if unspecified) and then loaded into the containerd image store.
282+
if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") {
283+
if !strings.Contains(output, "dest=") {
284+
result.NeedsLoading = true
285+
286+
if runtime.GOOS == "windows" {
287+
defaultDest = fmt.Sprintf(",dest=%s", defaultDestFile)
233288
}
234289
}
235290
}
291+
292+
var tags []string
236293
if tags = strutil.DedupeStrSlice(options.Tag); len(tags) > 0 {
237294
ref := tags[0]
238295
parsedReference, err := referenceutil.Parse(ref)
239296
if err != nil {
240-
return "", nil, false, "", nil, nil, err
297+
return result, err
241298
}
242299
output += ",name=" + parsedReference.String()
243300

244301
// pick the first tag and add it to output
245302
for idx, tag := range tags {
246303
parsedReference, err = referenceutil.Parse(tag)
247304
if err != nil {
248-
return "", nil, false, "", nil, nil, err
305+
return result, err
249306
}
250307
tags[idx] = parsedReference.String()
251308
}
252309
} else if len(tags) == 0 {
253310
output = output + ",dangling-name-prefix=<none>"
254311
}
312+
result.Tags = tags
313+
314+
// Add default destination file to output
315+
output += defaultDest
255316

256-
buildctlArgs = buildkitutil.BuildctlBaseArgs(options.BuildKitHost)
317+
// Extract destination file from output
318+
if strings.Contains(output, "dest=") {
319+
_, destFilePath, _ := strings.Cut(output, "dest=")
320+
result.DestFile = destFilePath
321+
}
257322

323+
buildctlArgs := buildkitutil.BuildctlBaseArgs(options.BuildKitHost)
258324
buildctlArgs = append(buildctlArgs, []string{
259325
"build",
260326
"--progress=" + options.Progress,
@@ -271,9 +337,9 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
271337
var err error
272338
dir, err = buildkitutil.WriteTempDockerfile(options.Stdin)
273339
if err != nil {
274-
return "", nil, false, "", nil, nil, err
340+
return result, err
275341
}
276-
cleanup = func() {
342+
result.Cleanup = func() {
277343
os.RemoveAll(dir)
278344
}
279345
} else {
@@ -286,12 +352,12 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
286352
}
287353
dir, file, err = buildkitutil.BuildKitFile(dir, file)
288354
if err != nil {
289-
return "", nil, false, "", nil, nil, err
355+
return result, err
290356
}
291357

292358
buildCtx, err := parseContextNames(options.ExtendedBuildContext)
293359
if err != nil {
294-
return "", nil, false, "", nil, nil, err
360+
return result, err
295361
}
296362

297363
for k, v := range buildCtx {
@@ -306,7 +372,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
306372
if isOCILayout := strings.HasPrefix(v, "oci-layout://"); isOCILayout {
307373
args, err := parseBuildContextFromOCILayout(k, v)
308374
if err != nil {
309-
return "", nil, false, "", nil, nil, err
375+
return result, err
310376
}
311377

312378
buildctlArgs = append(buildctlArgs, args...)
@@ -315,7 +381,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
315381

316382
path, err := filepath.Abs(v)
317383
if err != nil {
318-
return "", nil, false, "", nil, nil, err
384+
return result, err
319385
}
320386
buildctlArgs = append(buildctlArgs, fmt.Sprintf("--local=%s=%s", k, path))
321387
buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=context:%s=local:%s", k, k))
@@ -362,7 +428,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
362428
}
363429
}
364430
} else {
365-
return "", nil, false, "", nil, nil, fmt.Errorf("invalid build arg %q", ba)
431+
return result, fmt.Errorf("invalid build arg %q", ba)
366432
}
367433
}
368434

@@ -405,7 +471,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
405471
optAttestType := strings.TrimPrefix(optAttestType, "type=")
406472
buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=attest:%s=%s", optAttestType, optAttestAttrs))
407473
} else {
408-
return "", nil, false, "", nil, nil, fmt.Errorf("attestation type not specified")
474+
return result, fmt.Errorf("attestation type not specified")
409475
}
410476
}
411477

@@ -434,11 +500,11 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
434500
if options.IidFile != "" {
435501
file, err := os.CreateTemp("", "buildkit-meta-*")
436502
if err != nil {
437-
return "", nil, false, "", nil, cleanup, err
503+
return result, err
438504
}
439505
defer file.Close()
440-
metaFile = file.Name()
441-
buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile)
506+
result.MetaFile = file.Name()
507+
buildctlArgs = append(buildctlArgs, "--metadata-file="+result.MetaFile)
442508
}
443509

444510
if options.NetworkMode != "" {
@@ -453,7 +519,9 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
453519
}
454520
}
455521

456-
return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil
522+
result.BuildctlArgs = buildctlArgs
523+
524+
return result, nil
457525
}
458526

459527
func getDigestFromMetaFile(path string) (string, error) {

0 commit comments

Comments
 (0)