Annotation of pkgsrc/pkgtools/pkglint/files/pkglint.go, Revision 1.4
1.1 rillig 1: package main
2:
3: // based on pkglint.pl 1.896
4:
5: import (
6: "fmt"
7: "os"
8: "path"
9: "strings"
10: )
11:
12: const (
13: reDependencyCmp = `^((?:\$\{[\w_]+\}|[\w_\.+]|-[^\d])+)[<>]=?(\d[^-*?\[\]]*)$`
14: reDependencyWildcard = `^((?:\$\{[\w_]+\}|[\w_\.+]|-[^\d\[])+)-(?:\[0-9\]\*|\d[^-]*)$`
15: reMkCond = `^\.(\s*)(if|ifdef|ifndef|else|elif|endif|for|endfor|undef)(?:\s+([^\s#][^#]*?))?\s*(?:#.*)?$`
16: reMkInclude = `^\.\s*(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$`
17: reVarassign = `^ *([-*+A-Z_a-z0-9.${}\[]+?)\s*([!+:?]?=)\s*((?:\\#|[^#])*?)(?:\s*(#.*))?$`
18: rePkgname = `^([\w\-.+]+)-(\d(?:\w|\.\d)*)$`
19: rePkgbase = `(?:[+.\w]|-[A-Z_a-z])+`
20: rePkgversion = `\d(?:\w|\.\d)*`
21: )
22:
23: func explainRelativeDirs(line *Line) {
24: line.explain(
25: "Directories in the form \"../../category/package\" make it easier to",
26: "move a package around in pkgsrc, for example from pkgsrc-wip to the",
27: "main pkgsrc repository.")
28: }
29:
30: // Returns the pkgsrc top-level directory, relative to the given file or directory.
31: func findPkgsrcTopdir(fname string) string {
32: for _, dir := range []string{".", "..", "../..", "../../.."} {
33: if fileExists(fname + "/" + dir + "/mk/bsd.pkg.mk") {
34: return dir
35: }
36: }
37: return ""
38: }
39:
40: func loadPackageMakefile(fname string) []*Line {
41: defer tracecall("loadPackageMakefile", fname)()
42:
43: var mainLines, allLines []*Line
44: if !readMakefile(fname, &mainLines, &allLines) {
1.3 rillig 45: errorf(fname, noLines, "Cannot be read.")
1.1 rillig 46: return nil
47: }
48:
49: if G.opts.DumpMakefile {
1.3 rillig 50: debugf(G.currentDir, noLines, "Whole Makefile (with all included files) follows:")
1.1 rillig 51: for _, line := range allLines {
52: fmt.Printf("%s\n", line.String())
53: }
54: }
55:
56: determineUsedVariables(allLines)
57:
58: G.pkgContext.pkgdir = expandVariableWithDefault("PKGDIR", ".")
59: G.pkgContext.distinfoFile = expandVariableWithDefault("DISTINFO_FILE", "distinfo")
60: G.pkgContext.filesdir = expandVariableWithDefault("FILESDIR", "files")
61: G.pkgContext.patchdir = expandVariableWithDefault("PATCHDIR", "patches")
62:
63: if varIsDefined("PHPEXT_MK") {
64: if !varIsDefined("USE_PHP_EXT_PATCHES") {
65: G.pkgContext.patchdir = "patches"
66: }
67: if varIsDefined("PECL_VERSION") {
68: G.pkgContext.distinfoFile = "distinfo"
69: }
70: }
71:
72: _ = G.opts.DebugMisc &&
73: dummyLine.debugf("DISTINFO_FILE=%s", G.pkgContext.distinfoFile) &&
74: dummyLine.debugf("FILESDIR=%s", G.pkgContext.filesdir) &&
75: dummyLine.debugf("PATCHDIR=%s", G.pkgContext.patchdir) &&
76: dummyLine.debugf("PKGDIR=%s", G.pkgContext.pkgdir)
77:
78: return mainLines
79: }
80:
81: func determineUsedVariables(lines []*Line) {
82: re := regcomp(`(?:\$\{|\$\(|defined\(|empty\()([0-9+.A-Z_a-z]+)[:})]`)
83: for _, line := range lines {
84: rest := line.text
85: for {
86: m := re.FindStringSubmatchIndex(rest)
87: if m == nil {
88: break
89: }
90: varname := rest[m[2]:m[3]]
91: useVar(line, varname)
92: rest = rest[:m[0]] + rest[m[1]:]
93: }
94: }
95: }
96:
97: func extractUsedVariables(line *Line, text string) []string {
98: re := regcomp(`^(?:[^\$]+|\$[\$*<>?@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})`)
99: rest := text
100: var result []string
101: for {
102: m := re.FindStringSubmatchIndex(rest)
103: if m == nil {
104: break
105: }
106: varname := rest[negToZero(m[2]):negToZero(m[3])]
107: rest = rest[:m[0]] + rest[m[1]:]
108: if varname != "" {
109: result = append(result, varname)
110: }
111: }
112:
113: if rest != "" {
114: _ = G.opts.DebugMisc && line.debugf("extractUsedVariables: rest=%q", rest)
115: }
116: return result
117: }
118:
119: // Returns the type of the variable (maybe guessed based on the variable name),
120: // or nil if the type cannot even be guessed.
121: func getVariableType(line *Line, varname string) *Vartype {
122:
123: if vartype := G.globalData.vartypes[varname]; vartype != nil {
124: return vartype
125: }
126: if vartype := G.globalData.vartypes[varnameCanon(varname)]; vartype != nil {
127: return vartype
128: }
129:
130: if G.globalData.varnameToToolname[varname] != "" {
1.3 rillig 131: return &Vartype{lkNone, CheckvarShellCommand, []AclEntry{{"*", "u"}}, guNotGuessed}
1.1 rillig 132: }
133:
134: if m, toolvarname := match1(varname, `^TOOLS_(.*)`); m && G.globalData.varnameToToolname[toolvarname] != "" {
1.3 rillig 135: return &Vartype{lkNone, CheckvarPathname, []AclEntry{{"*", "u"}}, guNotGuessed}
1.1 rillig 136: }
137:
138: allowAll := []AclEntry{{"*", "adpsu"}}
139: allowRuntime := []AclEntry{{"*", "adsu"}}
140:
141: // Guess the datatype of the variable based on naming conventions.
142: var gtype *Vartype
143: switch {
144: case hasSuffix(varname, "DIRS"):
1.3 rillig 145: gtype = &Vartype{lkShell, CheckvarPathmask, allowRuntime, guGuessed}
1.1 rillig 146: case hasSuffix(varname, "DIR"), hasSuffix(varname, "_HOME"):
1.3 rillig 147: gtype = &Vartype{lkNone, CheckvarPathname, allowRuntime, guGuessed}
1.1 rillig 148: case hasSuffix(varname, "FILES"):
1.3 rillig 149: gtype = &Vartype{lkShell, CheckvarPathmask, allowRuntime, guGuessed}
1.1 rillig 150: case hasSuffix(varname, "FILE"):
1.3 rillig 151: gtype = &Vartype{lkNone, CheckvarPathname, allowRuntime, guGuessed}
1.1 rillig 152: case hasSuffix(varname, "PATH"):
1.3 rillig 153: gtype = &Vartype{lkNone, CheckvarPathlist, allowRuntime, guGuessed}
1.1 rillig 154: case hasSuffix(varname, "PATHS"):
1.3 rillig 155: gtype = &Vartype{lkShell, CheckvarPathname, allowRuntime, guGuessed}
1.1 rillig 156: case hasSuffix(varname, "_USER"):
1.3 rillig 157: gtype = &Vartype{lkNone, CheckvarUserGroupName, allowAll, guGuessed}
1.1 rillig 158: case hasSuffix(varname, "_GROUP"):
1.3 rillig 159: gtype = &Vartype{lkNone, CheckvarUserGroupName, allowAll, guGuessed}
1.1 rillig 160: case hasSuffix(varname, "_ENV"):
1.3 rillig 161: gtype = &Vartype{lkShell, CheckvarShellWord, allowRuntime, guGuessed}
1.1 rillig 162: case hasSuffix(varname, "_CMD"):
1.3 rillig 163: gtype = &Vartype{lkNone, CheckvarShellCommand, allowRuntime, guGuessed}
1.1 rillig 164: case hasSuffix(varname, "_ARGS"):
1.3 rillig 165: gtype = &Vartype{lkShell, CheckvarShellWord, allowRuntime, guGuessed}
1.1 rillig 166: case hasSuffix(varname, "_CFLAGS"), hasSuffix(varname, "_CPPFLAGS"), hasSuffix(varname, "_CXXFLAGS"), hasSuffix(varname, "_LDFLAGS"):
1.3 rillig 167: gtype = &Vartype{lkShell, CheckvarShellWord, allowRuntime, guGuessed}
1.1 rillig 168: case hasSuffix(varname, "_MK"):
1.3 rillig 169: gtype = &Vartype{lkNone, CheckvarUnchecked, allowAll, guGuessed}
1.1 rillig 170: case hasPrefix(varname, "PLIST."):
1.3 rillig 171: gtype = &Vartype{lkNone, CheckvarYes, allowAll, guGuessed}
1.1 rillig 172: }
173:
174: if gtype != nil {
175: _ = G.opts.DebugVartypes && line.debugf("The guessed type of %q is %v.", varname, gtype)
176: } else {
177: _ = G.opts.DebugVartypes && line.debugf("No type definition found for %q.", varname)
178: }
179: return gtype
180: }
181:
182: func resolveVariableRefs(text string) string {
183: defer tracecall("resolveVariableRefs", text)()
184:
185: visited := make(map[string]bool) // To prevent endless loops
186:
187: str := text
188: for {
189: replaced := regcomp(`\$\{([\w.]+)\}`).ReplaceAllStringFunc(str, func(m string) string {
190: varname := m[2 : len(m)-1]
191: if !visited[varname] {
192: visited[varname] = true
193: if ctx := G.pkgContext; ctx != nil {
194: if value, ok := ctx.varValue(varname); ok {
195: return value
196: }
197: }
198: if ctx := G.mkContext; ctx != nil {
199: if value, ok := ctx.varValue(varname); ok {
200: return value
201: }
202: }
203: }
204: return sprintf("${%s}", varname)
205: })
206: if replaced == str {
207: return replaced
208: }
209: str = replaced
210: }
211: }
212:
213: func expandVariableWithDefault(varname, defaultValue string) string {
214: line := G.pkgContext.vardef[varname]
215: if line == nil {
216: return defaultValue
217: }
218:
219: value := line.extra["value"].(string)
220: value = resolveVarsInRelativePath(value, true)
221: if containsVarRef(value) {
222: value = resolveVariableRefs(value)
223: }
224: _ = G.opts.DebugMisc && line.debugf("Expanded %q to %q", varname, value)
225: return value
226: }
227:
228: func getVariablePermissions(line *Line, varname string) string {
229: if vartype := getVariableType(line, varname); vartype != nil {
230: return vartype.effectivePermissions(line.fname)
231: }
232:
233: _ = G.opts.DebugMisc && line.debugf("No type definition found for %q.", varname)
234: return "adpsu"
235: }
236:
237: func checklineLength(line *Line, maxlength int) {
238: if len(line.text) > maxlength {
239: line.warnf("Line too long (should be no more than %d characters).", maxlength)
240: line.explain(
241: "Back in the old time, terminals with 80x25 characters were common.",
242: "And this is still the default size of many terminal emulators.",
243: "Moderately short lines also make reading easier.")
244: }
245: }
246:
247: func checklineValidCharacters(line *Line, reChar string) {
248: rest := regcomp(reChar).ReplaceAllString(line.text, "")
249: if rest != "" {
250: uni := ""
251: for _, c := range rest {
252: uni += sprintf(" %U", c)
253: }
254: line.warnf("Line contains invalid characters (%s).", uni[1:])
255: }
256: }
257:
258: func checklineValidCharactersInValue(line *Line, reValid string) {
259: varname := line.extra["varname"].(string)
260: value := line.extra["value"].(string)
261: rest := regcomp(reValid).ReplaceAllString(value, "")
262: if rest != "" {
263: uni := ""
264: for _, c := range rest {
265: uni += sprintf(" %U", c)
266: }
267: line.warnf("%s contains invalid characters (%s).", varname, uni[1:])
268: }
269: }
270:
271: func checklineTrailingWhitespace(line *Line) {
272: if hasSuffix(line.text, " ") || hasSuffix(line.text, "\t") {
273: line.notef("Trailing white-space.")
274: line.explain(
275: "When a line ends with some white-space, that space is in most cases",
276: "irrelevant and can be removed.")
277: line.replaceRegex(`\s+\n$`, "\n")
278: }
279: }
280:
281: func checklineRcsid(line *Line, prefixRe, suggestedPrefix string) bool {
282: defer tracecall("checklineRcsid", prefixRe, suggestedPrefix)()
283:
1.4 ! rillig 284: if !matches(line.text, `^`+prefixRe+`\$NetBSD(?::[^\$]+)?\$$`) {
1.1 rillig 285: line.errorf("Expected %q.", suggestedPrefix+"$"+"NetBSD$")
286: line.explain(
287: "Several files in pkgsrc must contain the CVS Id, so that their current",
288: "version can be traced back later from a binary package. This is to",
1.4 ! rillig 289: "ensure reproducible builds, for example for finding bugs.")
! 290: line.insertBefore(suggestedPrefix + "$" + "NetBSD$")
1.1 rillig 291: return false
292: }
293: return true
294: }
295:
296: func checklineRelativePath(line *Line, path string, mustExist bool) {
297: if !G.isWip && contains(path, "/wip/") {
298: line.errorf("A main pkgsrc package must not depend on a pkgsrc-wip package.")
299: }
300:
301: resolvedPath := resolveVarsInRelativePath(path, true)
302: if containsVarRef(resolvedPath) {
303: return
304: }
305:
306: abs := ifelseStr(hasPrefix(resolvedPath, "/"), "", G.currentDir+"/") + resolvedPath
307: if _, err := os.Stat(abs); err != nil {
308: if mustExist {
309: line.errorf("%q does not exist.", resolvedPath)
310: }
311: return
312: }
313:
314: switch {
315: case matches(path, `^\.\./\.\./[^/]+/[^/]`):
316: case hasPrefix(path, "../../mk/"):
317: // There need not be two directory levels for mk/ files.
318: case matches(path, `^\.\./mk/`) && G.curPkgsrcdir == "..":
319: // That's fine for category Makefiles.
320: case matches(path, `^\.\.`):
321: line.warnf("Invalid relative path %q.", path)
322: }
323: }
324:
325: func checkfileExtra(fname string) {
326: defer tracecall("checkfileExtra", fname)()
327:
328: if lines := LoadNonemptyLines(fname, false); lines != nil {
329: checklinesTrailingEmptyLines(lines)
330: }
331: }
332:
333: func checklinesMessage(lines []*Line) {
334: defer tracecall("checklinesMessage", lines[0].fname)()
335:
336: explanation := []string{
337: "A MESSAGE file should consist of a header line, having 75 \"=\"",
338: "characters, followed by a line containing only the RCS Id, then an",
339: "empty line, your text and finally the footer line, which is the",
340: "same as the header line."}
341:
342: if len(lines) < 3 {
343: lastLine := lines[len(lines)-1]
344: lastLine.warnf("File too short.")
345: lastLine.explain(explanation...)
346: return
347: }
348:
349: hline := strings.Repeat("=", 75)
350: if line := lines[0]; line.text != hline {
351: line.warnf("Expected a line of exactly 75 \"=\" characters.")
352: line.explain(explanation...)
353: }
354: checklineRcsid(lines[1], ``, "")
355: for _, line := range lines {
356: checklineLength(line, 80)
357: checklineTrailingWhitespace(line)
358: checklineValidCharacters(line, `[\t -~]`)
359: }
360: if lastLine := lines[len(lines)-1]; lastLine.text != hline {
361: lastLine.warnf("Expected a line of exactly 75 \"=\" characters.")
362: lastLine.explain(explanation...)
363: }
364: checklinesTrailingEmptyLines(lines)
365: }
366:
367: func checklineRelativePkgdir(line *Line, pkgdir string) {
368: checklineRelativePath(line, pkgdir, true)
369: pkgdir = resolveVarsInRelativePath(pkgdir, false)
370:
371: if m, otherpkgpath := match1(pkgdir, `^(?:\./)?\.\./\.\./([^/]+/[^/]+)$`); m {
372: if !fileExists(G.globalData.pkgsrcdir + "/" + otherpkgpath + "/Makefile") {
373: line.errorf("There is no package in %q.", otherpkgpath)
374: }
375:
376: } else {
377: line.warnf("%q is not a valid relative package directory.", pkgdir)
378: line.explain(
379: "A relative pathname always starts with \"../../\", followed",
380: "by a category, a slash and a the directory name of the package.",
381: "For example, \"../../misc/screen\" is a valid relative pathname.")
382: }
383: }
384:
385: func checkfileMk(fname string) {
386: defer tracecall("checkfileMk", fname)()
387:
388: lines := LoadNonemptyLines(fname, true)
389: if lines == nil {
390: return
391: }
392:
393: ParselinesMk(lines)
394: ChecklinesMk(lines)
395: saveAutofixChanges(lines)
396: }
397:
398: func checkfile(fname string) {
399: defer tracecall("checkfile", fname)()
400:
401: basename := path.Base(fname)
402: if matches(basename, `^(?:work.*|.*~|.*\.orig|.*\.rej)$`) {
403: if G.opts.Import {
1.3 rillig 404: errorf(fname, noLines, "Must be cleaned up before committing the package.")
1.1 rillig 405: }
406: return
407: }
408:
409: st, err := os.Lstat(fname)
410: if err != nil {
1.3 rillig 411: errorf(fname, noLines, "%s", err)
1.1 rillig 412: return
413: }
414:
415: if st.Mode().IsRegular() && st.Mode().Perm()&0111 != 0 && !isCommitted(fname) {
1.3 rillig 416: line := NewLine(fname, noLines, "", nil)
1.1 rillig 417: line.warnf("Should not be executable.")
418: line.explain(
419: "No package file should ever be executable. Even the INSTALL and",
420: "DEINSTALL scripts are usually not usable in the form they have in the",
421: "package, as the pathnames get adjusted during installation. So there is",
422: "no need to have any file executable.")
423: }
424:
425: switch {
426: case st.Mode().IsDir():
427: switch {
428: case basename == "files" || basename == "patches" || basename == "CVS":
429: // Ok
430: case matches(fname, `(?:^|/)files/[^/]*$`):
431: // Ok
432: case !isEmptyDir(fname):
1.3 rillig 433: warnf(fname, noLines, "Unknown directory name.")
1.1 rillig 434: }
435:
436: case st.Mode()&os.ModeSymlink != 0:
437: if !matches(basename, `^work`) {
1.3 rillig 438: warnf(fname, noLines, "Unknown symlink name.")
1.1 rillig 439: }
440:
441: case !st.Mode().IsRegular():
1.3 rillig 442: errorf(fname, noLines, "Only files and directories are allowed in pkgsrc.")
1.1 rillig 443:
444: case basename == "ALTERNATIVES":
445: if G.opts.CheckAlternatives {
446: checkfileExtra(fname)
447: }
448:
449: case basename == "buildlink3.mk":
450: if G.opts.CheckBuildlink3 {
451: if lines := LoadNonemptyLines(fname, true); lines != nil {
452: checklinesBuildlink3Mk(lines)
453: }
454: }
455:
456: case hasPrefix(basename, "DESCR"):
457: if G.opts.CheckDescr {
458: if lines := LoadNonemptyLines(fname, false); lines != nil {
459: checklinesDescr(lines)
460: }
461: }
462:
463: case hasPrefix(basename, "distinfo"):
464: if G.opts.CheckDistinfo {
465: if lines := LoadNonemptyLines(fname, false); lines != nil {
466: checklinesDistinfo(lines)
467: }
468: }
469:
470: case basename == "DEINSTALL" || basename == "INSTALL":
471: if G.opts.CheckInstall {
472: checkfileExtra(fname)
473: }
474:
475: case hasPrefix(basename, "MESSAGE"):
476: if G.opts.CheckMessage {
477: if lines := LoadNonemptyLines(fname, false); lines != nil {
478: checklinesMessage(lines)
479: }
480: }
481:
482: case matches(basename, `^patch-[-A-Za-z0-9_.~+]*[A-Za-z0-9_]$`):
483: if G.opts.CheckPatches {
484: if lines := LoadNonemptyLines(fname, false); lines != nil {
485: checklinesPatch(lines)
486: }
487: }
488:
489: case matches(fname, `(?:^|/)patches/manual[^/]*$`):
490: if G.opts.DebugUnchecked {
1.3 rillig 491: debugf(fname, noLines, "Unchecked file %q.", fname)
1.1 rillig 492: }
493:
494: case matches(fname, `(?:^|/)patches/[^/]*$`):
1.3 rillig 495: warnf(fname, noLines, "Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
1.1 rillig 496:
497: case matches(basename, `^(?:.*\.mk|Makefile.*)$`) && !matches(fname, `files/`) && !matches(fname, `patches/`):
498: if G.opts.CheckMk {
499: checkfileMk(fname)
500: }
501:
502: case hasPrefix(basename, "PLIST"):
503: if G.opts.CheckPlist {
504: if lines := LoadNonemptyLines(fname, false); lines != nil {
505: checklinesPlist(lines)
506: }
507: }
508:
509: case basename == "TODO" || basename == "README":
510: // Ok
511:
512: case hasPrefix(basename, "CHANGES-"):
513: // This only checks the file, but doesn’t register the changes globally.
514: G.globalData.loadDocChangesFromFile(fname)
515:
516: case matches(fname, `(?:^|/)files/[^/]*$`):
517: // Skip
518:
519: default:
1.3 rillig 520: warnf(fname, noLines, "Unexpected file found.")
1.1 rillig 521: if G.opts.CheckExtra {
522: checkfileExtra(fname)
523: }
524: }
525: }
526:
527: func checklinesTrailingEmptyLines(lines []*Line) {
528: max := len(lines)
529: last := max
530: for last > 1 && lines[last-1].text == "" {
531: last--
532: }
533: if last != max {
534: lines[last].notef("Trailing empty lines.")
535: }
536: }
537:
538: func matchVarassign(text string) (m bool, varname, op, value, comment string) {
539: if contains(text, "=") {
540: m, varname, op, value, comment = match4(text, reVarassign)
541: }
542: return
543: }
CVSweb <webmaster@jp.NetBSD.org>