Annotation of pkgsrc/pkgtools/pkglint/files/pkglint.go, Revision 1.26
1.1 rillig 1: package main
2:
3: import (
1.17 rillig 4: "fmt"
5: "netbsd.org/pkglint/getopt"
1.18 rillig 6: "netbsd.org/pkglint/histogram"
7: "netbsd.org/pkglint/regex"
8: "netbsd.org/pkglint/trace"
1.1 rillig 9: "os"
1.17 rillig 10: "os/user"
1.1 rillig 11: "path"
1.17 rillig 12: "path/filepath"
13: "runtime/pprof"
1.1 rillig 14: "strings"
15: )
16:
1.17 rillig 17: const confMake = "@BMAKE@"
18: const confVersion = "@VERSION@"
19:
20: func main() {
1.25 rillig 21: G.logOut = NewSeparatorWriter(os.Stdout)
22: G.logErr = NewSeparatorWriter(os.Stderr)
23: trace.Out = os.Stdout
1.17 rillig 24: os.Exit(new(Pkglint).Main(os.Args...))
25: }
26:
27: type Pkglint struct{}
28:
29: func (pkglint *Pkglint) Main(args ...string) (exitcode int) {
30: defer func() {
31: if r := recover(); r != nil {
32: if _, ok := r.(pkglintFatal); ok {
33: exitcode = 1
34: } else {
35: panic(r)
36: }
37: }
38: }()
39:
40: if exitcode := pkglint.ParseCommandLine(args); exitcode != nil {
41: return *exitcode
42: }
43:
44: if G.opts.Profiling {
45: f, err := os.Create("pkglint.pprof")
46: if err != nil {
47: dummyLine.Fatalf("Cannot create profiling file: %s", err)
48: }
49: pprof.StartCPUProfile(f)
50: defer pprof.StopCPUProfile()
51:
1.18 rillig 52: regex.Profiling = true
53: G.loghisto = histogram.New()
54: defer func() {
1.24 rillig 55: G.logOut.Write("")
56: G.loghisto.PrintStats("loghisto", G.logOut.out, 0)
1.18 rillig 57: regex.PrintStats()
58: }()
1.17 rillig 59: }
60:
61: for _, arg := range G.opts.args {
62: G.Todo = append(G.Todo, filepath.ToSlash(arg))
63: }
64: if len(G.Todo) == 0 {
65: G.Todo = []string{"."}
66: }
67:
68: G.globalData.Initialize()
69:
70: currentUser, err := user.Current()
71: if err == nil {
72: // On Windows, this is `Computername\Username`.
1.18 rillig 73: G.CurrentUsername = regex.Compile(`^.*\\`).ReplaceAllString(currentUser.Username, "")
1.17 rillig 74: }
75:
76: for len(G.Todo) != 0 {
77: item := G.Todo[0]
78: G.Todo = G.Todo[1:]
79: pkglint.CheckDirent(item)
80: }
81:
82: checkToplevelUnusedLicenses()
83: pkglint.PrintSummary()
84: if G.errors != 0 {
85: return 1
86: }
87: return 0
88: }
89:
90: func (pkglint *Pkglint) ParseCommandLine(args []string) *int {
91: gopts := &G.opts
92: opts := getopt.NewOptions()
93:
94: check := opts.AddFlagGroup('C', "check", "check,...", "enable or disable specific checks")
1.18 rillig 95: opts.AddFlagVar('d', "debug", &trace.Tracing, false, "log verbose call traces for debugging")
1.17 rillig 96: opts.AddFlagVar('e', "explain", &gopts.Explain, false, "explain the diagnostics or give further help")
97: opts.AddFlagVar('f', "show-autofix", &gopts.PrintAutofix, false, "show what pkglint can fix automatically")
98: opts.AddFlagVar('F', "autofix", &gopts.Autofix, false, "try to automatically fix some errors (experimental)")
99: opts.AddFlagVar('g', "gcc-output-format", &gopts.GccOutput, false, "mimic the gcc output format")
100: opts.AddFlagVar('h', "help", &gopts.PrintHelp, false, "print a detailed usage message")
101: opts.AddFlagVar('I', "dumpmakefile", &gopts.DumpMakefile, false, "dump the Makefile after parsing")
102: opts.AddFlagVar('i', "import", &gopts.Import, false, "prepare the import of a wip package")
103: opts.AddFlagVar('m', "log-verbose", &gopts.LogVerbose, false, "allow the same log message more than once")
1.24 rillig 104: opts.AddStrList('o', "only", &gopts.LogOnly, "only log messages containing the given text")
1.17 rillig 105: opts.AddFlagVar('p', "profiling", &gopts.Profiling, false, "profile the executing program")
106: opts.AddFlagVar('q', "quiet", &gopts.Quiet, false, "don't print a summary line when finishing")
107: opts.AddFlagVar('r', "recursive", &gopts.Recursive, false, "check subdirectories, too")
108: opts.AddFlagVar('s', "source", &gopts.PrintSource, false, "show the source lines together with diagnostics")
109: opts.AddFlagVar('V', "version", &gopts.PrintVersion, false, "print the version number of pkglint")
110: warn := opts.AddFlagGroup('W', "warning", "warning,...", "enable or disable groups of warnings")
111:
112: check.AddFlagVar("ALTERNATIVES", &gopts.CheckAlternatives, true, "check ALTERNATIVES files")
113: check.AddFlagVar("bl3", &gopts.CheckBuildlink3, true, "check buildlink3.mk files")
114: check.AddFlagVar("DESCR", &gopts.CheckDescr, true, "check DESCR file")
115: check.AddFlagVar("distinfo", &gopts.CheckDistinfo, true, "check distinfo file")
116: check.AddFlagVar("extra", &gopts.CheckExtra, false, "check various additional files")
117: check.AddFlagVar("global", &gopts.CheckGlobal, false, "inter-package checks")
118: check.AddFlagVar("INSTALL", &gopts.CheckInstall, true, "check INSTALL and DEINSTALL scripts")
119: check.AddFlagVar("Makefile", &gopts.CheckMakefile, true, "check Makefiles")
120: check.AddFlagVar("MESSAGE", &gopts.CheckMessage, true, "check MESSAGE file")
121: check.AddFlagVar("mk", &gopts.CheckMk, true, "check other .mk files")
122: check.AddFlagVar("patches", &gopts.CheckPatches, true, "check patches")
123: check.AddFlagVar("PLIST", &gopts.CheckPlist, true, "check PLIST files")
124:
125: warn.AddFlagVar("absname", &gopts.WarnAbsname, true, "warn about use of absolute file names")
126: warn.AddFlagVar("directcmd", &gopts.WarnDirectcmd, true, "warn about use of direct command names instead of Make variables")
127: warn.AddFlagVar("extra", &gopts.WarnExtra, false, "enable some extra warnings")
128: warn.AddFlagVar("order", &gopts.WarnOrder, false, "warn if Makefile entries are unordered")
129: warn.AddFlagVar("perm", &gopts.WarnPerm, false, "warn about unforeseen variable definition and use")
130: warn.AddFlagVar("plist-depr", &gopts.WarnPlistDepr, false, "warn about deprecated paths in PLISTs")
131: warn.AddFlagVar("plist-sort", &gopts.WarnPlistSort, false, "warn about unsorted entries in PLISTs")
132: warn.AddFlagVar("quoting", &gopts.WarnQuoting, false, "warn about quoting issues")
133: warn.AddFlagVar("space", &gopts.WarnSpace, false, "warn about inconsistent use of white-space")
134: warn.AddFlagVar("style", &gopts.WarnStyle, false, "warn about stylistic issues")
135: warn.AddFlagVar("types", &gopts.WarnTypes, true, "do some simple type checking in Makefiles")
136:
137: remainingArgs, err := opts.Parse(args)
138: if err != nil {
1.24 rillig 139: fmt.Fprintf(G.logErr.out, "%s\n\n", err)
140: opts.Help(G.logErr.out, "pkglint [options] dir...")
1.17 rillig 141: exitcode := 1
142: return &exitcode
143: }
144: gopts.args = remainingArgs
145:
146: if gopts.PrintHelp {
1.24 rillig 147: opts.Help(G.logOut.out, "pkglint [options] dir...")
1.17 rillig 148: exitcode := 0
149: return &exitcode
150: }
151:
152: if G.opts.PrintVersion {
1.24 rillig 153: fmt.Fprintf(G.logOut.out, "%s\n", confVersion)
1.17 rillig 154: exitcode := 0
155: return &exitcode
156: }
157:
158: return nil
159: }
160:
161: func (pkglint *Pkglint) PrintSummary() {
1.25 rillig 162: if !G.opts.Quiet && !G.opts.Autofix {
1.17 rillig 163: if G.errors != 0 || G.warnings != 0 {
1.24 rillig 164: G.logOut.Printf("%d %s and %d %s found.\n",
1.17 rillig 165: G.errors, ifelseStr(G.errors == 1, "error", "errors"),
166: G.warnings, ifelseStr(G.warnings == 1, "warning", "warnings"))
167: } else {
1.24 rillig 168: G.logOut.WriteLine("Looks fine.")
1.17 rillig 169: }
170: if G.explanationsAvailable && !G.opts.Explain {
1.24 rillig 171: G.logOut.WriteLine("(Run \"pkglint -e\" to show explanations.)")
1.17 rillig 172: }
1.25 rillig 173: if G.autofixAvailable && !G.opts.PrintAutofix {
1.24 rillig 174: G.logOut.WriteLine("(Run \"pkglint -fs\" to show what can be fixed automatically.)")
1.17 rillig 175: }
176: if G.autofixAvailable && !G.opts.Autofix {
1.24 rillig 177: G.logOut.WriteLine("(Run \"pkglint -F\" to automatically fix some issues.)")
1.17 rillig 178: }
179: }
180: }
181:
182: func (pkglint *Pkglint) CheckDirent(fname string) {
1.18 rillig 183: if trace.Tracing {
184: defer trace.Call1(fname)()
1.17 rillig 185: }
186:
187: st, err := os.Lstat(fname)
188: if err != nil || !st.Mode().IsDir() && !st.Mode().IsRegular() {
189: NewLineWhole(fname).Errorf("No such file or directory.")
190: return
191: }
192: isDir := st.Mode().IsDir()
193: isReg := st.Mode().IsRegular()
194:
195: G.CurrentDir = ifelseStr(isReg, path.Dir(fname), fname)
196: absCurrentDir := abspath(G.CurrentDir)
197: G.Wip = !G.opts.Import && matches(absCurrentDir, `/wip/|/wip$`)
198: G.Infrastructure = matches(absCurrentDir, `/mk/|/mk$`)
199: G.CurPkgsrcdir = findPkgsrcTopdir(G.CurrentDir)
200: if G.CurPkgsrcdir == "" {
201: NewLineWhole(fname).Errorf("Cannot determine the pkgsrc root directory for %q.", G.CurrentDir)
202: return
203: }
204:
205: switch {
206: case isDir && isEmptyDir(fname):
207: return
208: case isReg:
209: Checkfile(fname)
210: return
211: }
212:
213: switch G.CurPkgsrcdir {
214: case "../..":
215: checkdirPackage(relpath(G.globalData.Pkgsrcdir, G.CurrentDir))
216: case "..":
217: CheckdirCategory()
218: case ".":
219: CheckdirToplevel()
220: default:
221: NewLineWhole(fname).Errorf("Cannot check directories outside a pkgsrc tree.")
222: }
223: }
1.1 rillig 224:
225: // Returns the pkgsrc top-level directory, relative to the given file or directory.
226: func findPkgsrcTopdir(fname string) string {
1.18 rillig 227: for _, dir := range [...]string{".", "..", "../..", "../../.."} {
1.1 rillig 228: if fileExists(fname + "/" + dir + "/mk/bsd.pkg.mk") {
229: return dir
230: }
231: }
232: return ""
233: }
234:
1.6 rillig 235: func resolveVariableRefs(text string) string {
1.18 rillig 236: if trace.Tracing {
237: defer trace.Call1(text)()
1.1 rillig 238: }
239:
240: visited := make(map[string]bool) // To prevent endless loops
241:
242: str := text
243: for {
1.18 rillig 244: replaced := regex.Compile(`\$\{([\w.]+)\}`).ReplaceAllStringFunc(str, func(m string) string {
1.1 rillig 245: varname := m[2 : len(m)-1]
246: if !visited[varname] {
247: visited[varname] = true
1.6 rillig 248: if G.Pkg != nil {
249: if value, ok := G.Pkg.varValue(varname); ok {
1.1 rillig 250: return value
251: }
252: }
1.6 rillig 253: if G.Mk != nil {
254: if value, ok := G.Mk.VarValue(varname); ok {
1.1 rillig 255: return value
256: }
257: }
258: }
1.6 rillig 259: return "${" + varname + "}"
1.1 rillig 260: })
261: if replaced == str {
262: return replaced
263: }
264: str = replaced
265: }
266: }
267:
1.6 rillig 268: func CheckfileExtra(fname string) {
1.18 rillig 269: if trace.Tracing {
270: defer trace.Call1(fname)()
1.1 rillig 271: }
272:
1.6 rillig 273: if lines := LoadNonemptyLines(fname, false); lines != nil {
274: ChecklinesTrailingEmptyLines(lines)
275: }
1.1 rillig 276: }
277:
1.22 rillig 278: func ChecklinesDescr(lines []Line) {
1.18 rillig 279: if trace.Tracing {
1.22 rillig 280: defer trace.Call1(lines[0].Filename)()
1.1 rillig 281: }
282:
1.6 rillig 283: for _, line := range lines {
1.22 rillig 284: CheckLineLength(line, 80)
285: CheckLineTrailingWhitespace(line)
286: CheckLineValidCharacters(line, `[\t -~]`)
287: if contains(line.Text, "${") {
1.15 rillig 288: line.Notef("Variables are not expanded in the DESCR file.")
1.1 rillig 289: }
290: }
1.6 rillig 291: ChecklinesTrailingEmptyLines(lines)
1.1 rillig 292:
1.6 rillig 293: if maxlines := 24; len(lines) > maxlines {
294: line := lines[maxlines]
1.1 rillig 295:
1.6 rillig 296: line.Warnf("File too long (should be no more than %d lines).", maxlines)
1.15 rillig 297: Explain(
1.6 rillig 298: "The DESCR file should fit on a traditional terminal of 80x25",
299: "characters. It is also intended to give a _brief_ summary about",
300: "the package's contents.")
1.1 rillig 301: }
302:
1.6 rillig 303: SaveAutofixChanges(lines)
1.1 rillig 304: }
305:
1.22 rillig 306: func ChecklinesMessage(lines []Line) {
1.18 rillig 307: if trace.Tracing {
1.22 rillig 308: defer trace.Call1(lines[0].Filename)()
1.1 rillig 309: }
310:
1.26 ! rillig 311: explanation := []string{
! 312: "A MESSAGE file should consist of a header line, having 75 \"=\"",
! 313: "characters, followed by a line containing only the RCS Id, then an",
! 314: "empty line, your text and finally the footer line, which is the",
! 315: "same as the header line."}
1.1 rillig 316:
317: if len(lines) < 3 {
318: lastLine := lines[len(lines)-1]
1.15 rillig 319: lastLine.Warnf("File too short.")
1.26 ! rillig 320: Explain(explanation...)
1.1 rillig 321: return
322: }
323:
324: hline := strings.Repeat("=", 75)
1.22 rillig 325: if line := lines[0]; line.Text != hline {
1.26 ! rillig 326: fix := line.Autofix()
! 327: fix.Warnf("Expected a line of exactly 75 \"=\" characters.")
! 328: fix.Explain(explanation...)
! 329: fix.InsertBefore(hline)
! 330: fix.Apply()
! 331: CheckLineRcsid(lines[0], ``, "")
! 332: } else if 1 < len(lines) {
! 333: CheckLineRcsid(lines[1], ``, "")
1.1 rillig 334: }
335: for _, line := range lines {
1.22 rillig 336: CheckLineLength(line, 80)
337: CheckLineTrailingWhitespace(line)
338: CheckLineValidCharacters(line, `[\t -~]`)
1.1 rillig 339: }
1.22 rillig 340: if lastLine := lines[len(lines)-1]; lastLine.Text != hline {
1.26 ! rillig 341: fix := lastLine.Autofix()
! 342: fix.Warnf("Expected a line of exactly 75 \"=\" characters.")
! 343: fix.Explain(explanation...)
! 344: fix.InsertAfter(hline)
! 345: fix.Apply()
1.1 rillig 346: }
1.6 rillig 347: ChecklinesTrailingEmptyLines(lines)
1.26 ! rillig 348:
! 349: SaveAutofixChanges(lines)
1.1 rillig 350: }
351:
1.6 rillig 352: func CheckfileMk(fname string) {
1.18 rillig 353: if trace.Tracing {
354: defer trace.Call1(fname)()
1.1 rillig 355: }
356:
357: lines := LoadNonemptyLines(fname, true)
358: if lines == nil {
359: return
360: }
361:
1.6 rillig 362: NewMkLines(lines).Check()
363: SaveAutofixChanges(lines)
1.1 rillig 364: }
365:
1.6 rillig 366: func Checkfile(fname string) {
1.18 rillig 367: if trace.Tracing {
368: defer trace.Call1(fname)()
1.6 rillig 369: }
1.1 rillig 370:
371: basename := path.Base(fname)
1.6 rillig 372: if hasPrefix(basename, "work") || hasSuffix(basename, "~") || hasSuffix(basename, ".orig") || hasSuffix(basename, ".rej") {
1.1 rillig 373: if G.opts.Import {
1.15 rillig 374: NewLineWhole(fname).Errorf("Must be cleaned up before committing the package.")
1.1 rillig 375: }
376: return
377: }
378:
379: st, err := os.Lstat(fname)
380: if err != nil {
1.8 rillig 381: NewLineWhole(fname).Errorf("%s", err)
1.1 rillig 382: return
383: }
384:
385: if st.Mode().IsRegular() && st.Mode().Perm()&0111 != 0 && !isCommitted(fname) {
1.6 rillig 386: line := NewLine(fname, 0, "", nil)
1.15 rillig 387: line.Warnf("Should not be executable.")
388: Explain(
1.6 rillig 389: "No package file should ever be executable. Even the INSTALL and",
390: "DEINSTALL scripts are usually not usable in the form they have in",
391: "the package, as the pathnames get adjusted during installation.",
392: "So there is no need to have any file executable.")
1.1 rillig 393: }
394:
395: switch {
396: case st.Mode().IsDir():
397: switch {
1.23 rillig 398: case basename == "files" || basename == "patches" || isIgnoredFilename(basename):
1.1 rillig 399: // Ok
400: case matches(fname, `(?:^|/)files/[^/]*$`):
401: // Ok
402: case !isEmptyDir(fname):
1.15 rillig 403: NewLineWhole(fname).Warnf("Unknown directory name.")
1.1 rillig 404: }
405:
406: case st.Mode()&os.ModeSymlink != 0:
1.9 rillig 407: if !hasPrefix(basename, "work") {
1.15 rillig 408: NewLineWhole(fname).Warnf("Unknown symlink name.")
1.1 rillig 409: }
410:
411: case !st.Mode().IsRegular():
1.15 rillig 412: NewLineWhole(fname).Errorf("Only files and directories are allowed in pkgsrc.")
1.1 rillig 413:
414: case basename == "ALTERNATIVES":
415: if G.opts.CheckAlternatives {
1.6 rillig 416: CheckfileExtra(fname)
1.1 rillig 417: }
418:
419: case basename == "buildlink3.mk":
420: if G.opts.CheckBuildlink3 {
421: if lines := LoadNonemptyLines(fname, true); lines != nil {
1.6 rillig 422: ChecklinesBuildlink3Mk(NewMkLines(lines))
1.1 rillig 423: }
424: }
425:
426: case hasPrefix(basename, "DESCR"):
427: if G.opts.CheckDescr {
428: if lines := LoadNonemptyLines(fname, false); lines != nil {
1.6 rillig 429: ChecklinesDescr(lines)
1.1 rillig 430: }
431: }
432:
1.6 rillig 433: case basename == "distinfo":
1.1 rillig 434: if G.opts.CheckDistinfo {
435: if lines := LoadNonemptyLines(fname, false); lines != nil {
1.6 rillig 436: ChecklinesDistinfo(lines)
1.1 rillig 437: }
438: }
439:
440: case basename == "DEINSTALL" || basename == "INSTALL":
441: if G.opts.CheckInstall {
1.6 rillig 442: CheckfileExtra(fname)
1.1 rillig 443: }
444:
445: case hasPrefix(basename, "MESSAGE"):
446: if G.opts.CheckMessage {
447: if lines := LoadNonemptyLines(fname, false); lines != nil {
1.6 rillig 448: ChecklinesMessage(lines)
1.1 rillig 449: }
450: }
451:
452: case matches(basename, `^patch-[-A-Za-z0-9_.~+]*[A-Za-z0-9_]$`):
453: if G.opts.CheckPatches {
454: if lines := LoadNonemptyLines(fname, false); lines != nil {
1.6 rillig 455: ChecklinesPatch(lines)
1.1 rillig 456: }
457: }
458:
459: case matches(fname, `(?:^|/)patches/manual[^/]*$`):
1.18 rillig 460: if trace.Tracing {
461: trace.Step1("Unchecked file %q.", fname)
1.1 rillig 462: }
463:
464: case matches(fname, `(?:^|/)patches/[^/]*$`):
1.15 rillig 465: NewLineWhole(fname).Warnf("Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
1.1 rillig 466:
467: case matches(basename, `^(?:.*\.mk|Makefile.*)$`) && !matches(fname, `files/`) && !matches(fname, `patches/`):
468: if G.opts.CheckMk {
1.6 rillig 469: CheckfileMk(fname)
1.1 rillig 470: }
471:
472: case hasPrefix(basename, "PLIST"):
473: if G.opts.CheckPlist {
474: if lines := LoadNonemptyLines(fname, false); lines != nil {
1.6 rillig 475: ChecklinesPlist(lines)
1.1 rillig 476: }
477: }
478:
479: case basename == "TODO" || basename == "README":
480: // Ok
481:
482: case hasPrefix(basename, "CHANGES-"):
1.17 rillig 483: // This only checks the file, but doesn't register the changes globally.
1.1 rillig 484: G.globalData.loadDocChangesFromFile(fname)
485:
486: case matches(fname, `(?:^|/)files/[^/]*$`):
487: // Skip
488:
1.9 rillig 489: case basename == "spec":
490: // Ok in regression tests
491:
1.1 rillig 492: default:
1.15 rillig 493: NewLineWhole(fname).Warnf("Unexpected file found.")
1.1 rillig 494: if G.opts.CheckExtra {
1.6 rillig 495: CheckfileExtra(fname)
1.1 rillig 496: }
497: }
498: }
499:
1.22 rillig 500: func ChecklinesTrailingEmptyLines(lines []Line) {
1.1 rillig 501: max := len(lines)
502: last := max
1.22 rillig 503: for last > 1 && lines[last-1].Text == "" {
1.1 rillig 504: last--
505: }
506: if last != max {
1.15 rillig 507: lines[last].Notef("Trailing empty lines.")
1.1 rillig 508: }
509: }
CVSweb <webmaster@jp.NetBSD.org>