diff --git a/.tools/internal/ebuild/embed/verify-manifest.sh b/.tools/internal/ebuild/embed/verify-manifest.sh index f5b918d..af67520 100644 --- a/.tools/internal/ebuild/embed/verify-manifest.sh +++ b/.tools/internal/ebuild/embed/verify-manifest.sh @@ -4,7 +4,7 @@ # # Valid exit codes are: # 0 - Manifest is up-to-date and valid -# 1 - General error ocurring during validation +# 1 - General error occurring during validation # 2 - Manifest is invalid or out-of-sync # # Usage: verify-manifest.sh [package-dir] diff --git a/.tools/internal/ebuild/manifest.go b/.tools/internal/ebuild/manifest.go index 3998b4a..ecc593c 100644 --- a/.tools/internal/ebuild/manifest.go +++ b/.tools/internal/ebuild/manifest.go @@ -16,10 +16,17 @@ package ebuild import ( + "context" _ "embed" + "fmt" "io" + "io/fs" "os/exec" + "path" + "path/filepath" + "strings" + "github.com/jaredallard/overlay/.tools/internal/steps/stepshelpers" "github.com/pkg/errors" ) @@ -27,7 +34,7 @@ import ( // Manifest files. // //go:embed embed/verify-manifest.sh -var manifestValidationScript string +var manifestValidationScript []byte // gentooImage is the docker image used for validating Manifest files. var gentooImage = "ghcr.io/jaredallard/overlay:updater" @@ -39,29 +46,89 @@ var ( ErrManifestInvalid = errors.New("manifest is out of date or invalid") ) -// ValidateManifest ensures that the manifest at the provided path is -// valid for the given ebuild. This requires docker to be installed on -// the host and running. -func ValidateManifest(stdout, stderr io.Writer, packageDir, packageName string) error { - cmd := exec.Command( - "docker", "run", - // Ensures we can use the network-sandbox feature. - "--privileged", - // Run bash and mount the ebuild repository at a predictable path. - "--rm", "--entrypoint", "bash", "-v"+packageDir+":/ebuild/src:ro", - gentooImage, "-c", manifestValidationScript, "", packageName, - ) - cmd.Stdout = stdout - cmd.Stderr = stderr - if err := cmd.Run(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - if exitErr.ExitCode() == 2 { - return ErrManifestInvalid - } +// copyDirectoryIntoContainer copies all files in the srcPath/ into +// destPath/ in the container. +func copyDirectoryIntoContainer(ctx context.Context, containerID, srcPath, destPath string) error { + directories := make(map[string]struct{}) + + if err := filepath.WalkDir(srcPath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err } - return errors.Wrap(err, "unknown error while validating manifest") + // We skip directories. + if d.IsDir() { + return nil + } + + relPath, err := filepath.Rel(srcPath, path) + if err != nil { + return fmt.Errorf("could not create relative path for %q: %w", path, err) + } + + dir := filepath.Dir(relPath) + if _, ok := directories[dir]; !ok { + if err := stepshelpers.RunCommandInContainer(ctx, containerID, "mkdir", "-p", filepath.Join(destPath, dir)); err != nil { + return fmt.Errorf("failed to ensure directory %q: %w", dir, err) + } + directories[dir] = struct{}{} + } + + if err := stepshelpers.CopyFileToContainer(ctx, containerID, filepath.Join(srcPath, relPath), filepath.Join(destPath, relPath)); err != nil { + return fmt.Errorf("failed to copy %q into container: %w", relPath, err) + } + + return nil + }); err != nil { + return fmt.Errorf("failed to walk srcDir: %w", err) + } + + return nil +} + +// ValidateManifest ensures that the manifest at the provided path is +// valid for the given ebuild. This requires docker to be installed on +// the host and running. +func ValidateManifest(stdout, stderr io.Writer, overlayDir, packageName string) error { + ctx := context.TODO() + + bid, err := exec.Command( + "docker", "run", "-d", "--rm", "--entrypoint", "sleep", gentooImage, "infinity", + ).Output() + if err != nil { + var execErr *exec.ExitError + if errors.As(err, &execErr) { + return fmt.Errorf("failed to run container: %s", string(execErr.Stderr)) + } + + return fmt.Errorf("failed to run container: %w", err) + } + containerID := strings.TrimSpace(string(bid)) + defer exec.Command("docker", "stop", containerID) //nolint:errcheck // Why: best effort + + lclPkgDir := filepath.Join(overlayDir, packageName) + + containerOverlayDir := "/ebuild/src" + containerPkgDir := path.Join(containerOverlayDir, packageName) + + if err := copyDirectoryIntoContainer(ctx, containerID, lclPkgDir, containerPkgDir); err != nil { + return fmt.Errorf("failed to copy ebuild contents into container: %w", err) + } + + if err := copyDirectoryIntoContainer(ctx, containerID, filepath.Join(overlayDir, "metadata"), "/ebuild/src/metadata"); err != nil { + return fmt.Errorf("failed to copy metadata directory into container: %w", err) + } + + if err := stepshelpers.CopyFileBytesToContainer(ctx, containerID, manifestValidationScript, "/verify-manifest.sh"); err != nil { + return fmt.Errorf("failed to copy validation script into container: %w", err) + } + + if err := stepshelpers.RunCommandInContainer(ctx, containerID, "chmod", "+x", "/verify-manifest.sh"); err != nil { + return fmt.Errorf("failed to mark validation script as executable: %w", err) + } + + if err := stepshelpers.RunCommandInContainer(ctx, containerID, "/verify-manifest.sh", packageName); err != nil { + return fmt.Errorf("ebuild failed to lint: %w", err) } return nil