This repository has been archived by the owner on Feb 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
aaa17e8
commit 28021ef
Showing
19 changed files
with
759 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
Oops, something went wrong.