File build.go of Package obs-service-dotnet_packages

package main

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/client"
)

func build(ctx context.Context) error {
	srcDir, err := os.MkdirTemp("", "obs-service-dotnet-packages-src-*")
	if err != nil {
		return err
	}
	defer os.RemoveAll(srcDir)
	solutions, err := extractArchive(ctx, options.archive, srcDir)
	if err != nil {
		return err
	}
	outDir, err := os.MkdirTemp("", "obs-service-dotnet-packages-out-*")
	if err != nil {
		return err
	}
	defer os.RemoveAll(outDir)

	dc, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		return fmt.Errorf("failed to create docker client: %w", err)
	}
	c, err := dc.ContainerCreate(
		ctx,
		&container.Config{
			Cmd:        []string{"sleep", "inf"},
			Image:      "registry.suse.com/bci/dotnet-sdk:" + options.tag,
			WorkingDir: "/src",
		},
		&container.HostConfig{
			Mounts: []mount.Mount{
				{
					Type:   mount.TypeBind,
					Source: srcDir,
					Target: "/src",
					BindOptions: &mount.BindOptions{
						CreateMountpoint: true,
					},
				},
				{
					Type:   mount.TypeBind,
					Source: outDir,
					Target: "/out",
					BindOptions: &mount.BindOptions{
						CreateMountpoint: true,
					},
				},
			},
			AutoRemove: true,
		},
		nil,
		nil,
		"")
	if err != nil {
		return fmt.Errorf("failed to create container: %w", err)
	}
	defer func() {
		err := dc.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
		if err != nil {
			slog.ErrorContext(ctx, "failed to remove container", "error", err)
		}
	}()

	if err := dc.ContainerStart(ctx, c.ID, container.StartOptions{}); err != nil {
		return err
	}

	// Use a local function here to ensure we always set permissions after
	// running dotnet restore.
	err = func() error {
		defer func() {
			if err := setPermissions(ctx, dc, c.ID); err != nil {
				slog.ErrorContext(
					ctx,
					"failed to reset permissions, temporary files may be left behind",
					"error", err,
					"src", srcDir,
					"out", outDir,
				)
			}
		}()

		for _, solution := range solutions {
			if err := restore(ctx, dc, c.ID, solution); err != nil {
				return fmt.Errorf("error restoring %s: %w", solution, err)
			}
		}

		return nil
	}()
	if err != nil {
		return err
	}

	if err := cleanup(ctx, outDir); err != nil {
		slog.WarnContext(ctx, "failed to clean up, archive might be larger than needed", "error", err)
	}

	outBase := options.output
	if options.outDir != "" {
		outBase = filepath.Join(options.outDir, options.output)
	}
	slog.InfoContext(ctx, "creating output archive", "base name", outBase)
	if err := createArchive(outDir, outBase, options.compression); err != nil {
		return fmt.Errorf("error creating output archive: %w", err)
	}
	return nil
}

func execInContainer(ctx context.Context, dc *client.Client, containerID string, cmd ...string) error {
	exec, err := dc.ContainerExecCreate(
		ctx,
		containerID,
		container.ExecOptions{
			Tty:          true,
			AttachStdout: true,
			AttachStderr: true,
			Cmd:          cmd,
		})
	if err != nil {
		return err
	}
	resp, err := dc.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{Tty: true})
	if err != nil {
		return err
	}
	if err := dc.ContainerExecStart(ctx, exec.ID, container.ExecStartOptions{Tty: true}); err != nil {
		return err
	}
	_, _ = io.Copy(io.Discard, resp.Reader)
	return nil
}

func restore(ctx context.Context, dc *client.Client, containerID, solutionPath string) error {
	slog.InfoContext(ctx, "restoring solution", "solution", solutionPath)
	return execInContainer(
		ctx, dc, containerID,
		"dotnet", "restore", solutionPath,
		"--packages", "/out",
		"--verbosity", "detailed",
		"--locked-mode")
}

func setPermissions(ctx context.Context, dc *client.Client, containerID string) error {
	slog.InfoContext(ctx, "resetting file permissions")
	return execInContainer(
		ctx, dc, containerID,
		"chown", "--recursive", "--reference=/src", "/src", "/out")
}

func cleanup(ctx context.Context, workDir string) error {
	slog.InfoContext(ctx, "removing extraneous files")
	match := func(path string, patterns ...string) bool {
		for _, pattern := range patterns {
			if m, err := filepath.Match(pattern, path); err != nil {
				panic(fmt.Sprintf("bad pattern %q", pattern))
			} else if m {
				return true
			}
		}
		return false
	}
	return fs.WalkDir(os.DirFS(workDir), ".", func(path string, d fs.DirEntry, err error) error {
		switch {
		case match(path, "*", "*/*"):
			if d.IsDir() {
				return nil
			}
			slog.DebugContext(ctx, "removing non-directory", "path", path)
			if err := os.RemoveAll(filepath.Join(workDir, path)); err != nil {
				return fmt.Errorf("failed to remove file %s: %w", path, err)
			}
			return nil
		case match(path, "*/*/*.nupkg", "*/*/*.nupkg.sha512", "*/*/*.nuspec"):
			if d.IsDir() {
				slog.DebugContext(ctx, "removing directory", "path", path)
				if err := os.RemoveAll(filepath.Join(workDir, path)); err != nil {
					return fmt.Errorf("failed to remove directory %s: %w", path, err)
				}
				return fs.SkipDir
			}
			return nil
		default:
			slog.DebugContext(ctx, "removing extra", "path", path)
			if err := os.RemoveAll(filepath.Join(workDir, path)); err != nil {
				return fmt.Errorf("failed to remove %s: %w", path, err)
			}
			if d.IsDir() {
				return fs.SkipDir
			}
			return nil
		}
	})
}
openSUSE Build Service is sponsored by