Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
Add linter to lint ebuilds (#11)
Browse files Browse the repository at this point in the history
Adds a command named 'elint' to lint ebuilds. This mainly ensures that
they have a few required fields (`DESCRIPTION` and `LICENSE` right now)
and that their `Manifest` files are valid. This is helpful for ensuring
that we don't accidentally ever have broken ebuilds in this repository.

Includes a Docker image for it to use so that the linter can be ran on
any operating system (`ebuild` only exists on Gentoo). We use the stage3
docker image and run `emerge-webrsync` on top of it to keep lint times
low.

Includes a Github action to build and push that Docker image (mostly
meant to keep it updated periodically, since you'd need to push it up
manually to get it working if it truely broke) as well as an action to
run `elint` on all PRs and pushes to `main`.
  • Loading branch information
jaredallard authored Aug 14, 2023
1 parent aaa17e8 commit 28021ef
Show file tree
Hide file tree
Showing 19 changed files with 759 additions and 6 deletions.
10 changes: 10 additions & 0 deletions .elint/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This image is used as the base image for running Manifest validations.
FROM gentoo/stage3:latest

LABEL org.opencontainers.image.source https://github.com/getoutreach/overlay

# run emerge-webrsync to download the latest portage tree. This is
# required to run ebuild commands as eclasses are distributed along with
# the portage tree. We run this once to prevent hitting mirrors
# excessively.
RUN emerge-webrsync
11 changes: 11 additions & 0 deletions .elint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# elint

An ebuild static linter and validator.

## Linting Configuration

This linter configuration is used by the CI to ensure that the overlay
is in a consistent state. It does the following for all ebuilds:

- Ensures that `DESCRIPTION` and `LICENSE` are set.
- Validates that the `Manifest` file is up-to-date.
191 changes: 191 additions & 0 deletions .elint/cmd/elint/elint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright (C) 2023 Outreach <https://outreach.io>
//
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License version
// 2 as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// Package main implements a linter for Gentoo ebuilds. It is intended
// to be ran only on the getoutreach/overlay repository. It mainly
// handles:
// - Ensuring ebuilds have certain variables set.
// - Ensuring that Manifest files have been updated.
package main

import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/egym-playground/go-prefix-writer/prefixer"
"github.com/fatih/color"
"github.com/getoutreach/overlay/.elint/internal/ebuild"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// contains color helpers
var (
bold = color.New(color.Bold).SprintFunc()
faint = color.New(color.Faint).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
green = color.New(color.FgGreen).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
)

// main is the entrypoint for the linter.
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

// lint lints the provided packageName in the provided workDir.
func lint(workDir, packageName string) (errOutput string) {
// packageName is the format of "category/package".
packageName = strings.TrimSuffix(packageName, "/")
packagePath := packageName

// find the first ebuild in the package directory.
files, err := os.ReadDir(packagePath)
if err != nil {
return "failed to read package directory"
}

var ebuildPath string
for _, file := range files {
if filepath.Ext(file.Name()) == ".ebuild" {
ebuildPath = filepath.Join(packagePath, file.Name())
break
}
}
if ebuildPath == "" {
return "no ebuild found in package directory"
}

e, err := ebuild.Parse(ebuildPath)
if err != nil {
return errors.Wrap(err, "failed to parse ebuild").Error()
}

if e.Description == "" {
return "ebuild: missing DESCRIPTION " + "(" + filepath.Base(ebuildPath) + ")"
}

if e.License == "" {
return "ebuild: missing LICENSE " + "(" + filepath.Base(ebuildPath) + ")"
}

// Validate that the Manifest file is up-to-date for the package.
var buf bytes.Buffer
out := prefixer.New(&buf, func() string { return color.New(color.Faint).Sprint(" => ") })
if err := ebuild.ValidateManifest(
out, out,
workDir,
packageName,
); err != nil {
errOutput = buf.String()
if errors.Is(err, ebuild.ErrManifestInvalid) {
errOutput += yellow("Manifest is out-of-date or otherwise invalid. Regenerate with 'ebuild <.ebuild> manifest'")
return
}

errOutput += "Manifest validation failed for an unknown reason (err: " + err.Error() + ")"
return
}

errOutput = ""
return
}

var rootCmd = &cobra.Command{
Use: "linter [packageName...]",
Short: "Ensures ebuilds pass lint checks as well as being valid.",
Long: "If no arguments are passed, all packages in the current directory will be linted.\n" +
"If arguments are passed, only those packages will be linted.",
Run: func(cmd *cobra.Command, args []string) {
workDir, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, "failed to get working directory:", err)
os.Exit(1)
}

if len(args) == 0 {
// If no arguments are passed, lint all packages in the current
// directory.
files, err := os.ReadDir(workDir)
if err != nil {
fmt.Fprintln(os.Stderr, "failed to read directory:", err)
os.Exit(1)
}

for _, file := range files {
if !file.IsDir() {
continue
}

// skip hidden directories.
if strings.HasPrefix(file.Name(), ".") {
continue
}

subDir := filepath.Join(workDir, file.Name())
subFiles, err := os.ReadDir(subDir)
if err != nil {
fmt.Fprintln(os.Stderr, "failed to read directory", subDir+":", err)
os.Exit(1)
}
for _, subFile := range subFiles {
if !subFile.IsDir() {
continue
}

// join the subdirectory with the subdir to get the full
// package name (category/package).
args = append(args, filepath.Join(file.Name(), subFile.Name()))
}
}
}

if len(args) == 0 {
fmt.Fprintln(os.Stdout, "no packages to lint")
os.Exit(0)
}

if len(args) == 1 {
fmt.Println("Linting package", args[0])
} else {
fmt.Println("Linting all packages in the current directory")
}

for _, packageName := range args {
packageNameFaint := faint(packageName)
fmt.Print(packageNameFaint, bold(" ..."))

if err := lint(workDir, packageName); err != "" {
// update the line to be red.
fmt.Printf("\r%s %s\n", packageNameFaint, red("✘ "))

// print the error and then exit
fmt.Fprintln(os.Stderr, err)
fmt.Println("Linting failed for package", packageName)
os.Exit(1)
}

// update the line to be green.
fmt.Printf("\r%s %s\n", packageNameFaint, green("✔ "))
}

fmt.Println("All package(s) linted successfully")
},
}
18 changes: 18 additions & 0 deletions .elint/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/getoutreach/overlay/.elint

go 1.20

require (
github.com/egym-playground/go-prefix-writer v0.0.0-20180609083313-7326ea162eca
github.com/fatih/color v1.15.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.7.0
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.6.0 // indirect
)
24 changes: 24 additions & 0 deletions .elint/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/egym-playground/go-prefix-writer v0.0.0-20180609083313-7326ea162eca h1:sWNMfkKG8GW1pGUyNlbsWq6f04pFgcsomY+Fly8XdB4=
github.com/egym-playground/go-prefix-writer v0.0.0-20180609083313-7326ea162eca/go.mod h1:Ar+qogA+fkjeUR18xJfFzrMSjfs/sCPO+yjVvhXpyEg=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 28021ef

Please sign in to comment.