build(lint): first pass at linter without requiring volume mounts
All checks were successful
Test / elint (push) Successful in 38s

act, much like CircleCI, doesn't allow us to have easily accessible
volume mounts between the CI system and the Docker host. So, much like
the build of the updater, the linter now no longer requires volume
mounts to lint ebuilds.

This implementation is decently ineffecient and will be improved over
time.
This commit is contained in:
Jared Allard 2025-03-09 16:35:45 -07:00
parent 21e1d47d5f
commit c4274316fa
Signed by: jaredallard
SSH key fingerprint: SHA256:wyRyyv28jBYw8Yp/oABNPUYvbGd6hyZj23XVXEm5G/U
2 changed files with 90 additions and 23 deletions

View file

@ -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]

View file

@ -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