Annotation of pkgsrc/pkgtools/pkglint/files/plist.go, Revision 1.58
1.33 rillig 1: package pkglint
1.1 rillig 2:
3: import (
1.33 rillig 4: "netbsd.org/pkglint/textproc"
1.4 rillig 5: "sort"
1.1 rillig 6: "strings"
7: )
8:
1.42 rillig 9: func CheckLinesPlist(pkg *Package, lines *Lines) {
1.12 rillig 10: if trace.Tracing {
1.45 rillig 11: defer trace.Call(lines.Filename)()
1.4 rillig 12: }
1.1 rillig 13:
1.42 rillig 14: idOk := lines.CheckCvsID(0, `@comment `, "@comment ")
1.1 rillig 15:
1.42 rillig 16: if idOk && lines.Len() == 1 {
1.40 rillig 17: line := lines.Lines[0]
1.50 rillig 18: line.Errorf("PLIST files must not be empty.")
1.40 rillig 19: line.Explain(
1.1 rillig 20: "One reason for empty PLISTs is that this is a newly created package",
1.35 rillig 21: sprintf("and that the author didn't run %q after installing the files.", bmake("print-PLIST")),
1.1 rillig 22: "",
1.35 rillig 23: "For most Perl packages, the final PLIST is generated automatically.",
24: "Since the source PLIST is not used at all, it can be removed for these packages.",
1.1 rillig 25: "",
1.35 rillig 26: "Meta packages also don't need a PLIST file",
27: "since their only purpose is to declare dependencies.")
1.42 rillig 28: return
1.1 rillig 29: }
30:
1.48 rillig 31: ck := NewPlistChecker(pkg)
1.4 rillig 32: ck.Check(lines)
33: }
34:
35: type PlistChecker struct {
1.39 rillig 36: pkg *Package
1.47 rillig 37: allFiles map[RelPath]*PlistLine
38: allDirs map[RelPath]*PlistLine
1.48 rillig 39: lastFname RelPath
1.39 rillig 40: once Once
41: nonAsciiAllowed bool
1.4 rillig 42: }
43:
1.48 rillig 44: func NewPlistChecker(pkg *Package) *PlistChecker {
45: return &PlistChecker{
46: pkg,
47: make(map[RelPath]*PlistLine),
48: make(map[RelPath]*PlistLine),
49: "",
50: Once{},
51: false}
52: }
53:
1.43 rillig 54: func (ck *PlistChecker) Load(lines *Lines) []*PlistLine {
1.48 rillig 55: plines := ck.newLines(lines)
1.4 rillig 56: ck.collectFilesAndDirs(plines)
57:
1.43 rillig 58: if lines.BaseName == "PLIST.common_end" {
1.45 rillig 59: commonLines := Load(lines.Filename.TrimSuffix("_end"), NotEmpty)
1.27 rillig 60: if commonLines != nil {
1.48 rillig 61: ck.collectFilesAndDirs(ck.newLines(commonLines))
1.1 rillig 62: }
63: }
64:
1.43 rillig 65: return plines
66: }
67:
68: func (ck *PlistChecker) Check(plainLines *Lines) {
69: plines := ck.Load(plainLines)
70:
1.4 rillig 71: for _, pline := range plines {
1.35 rillig 72: ck.checkLine(pline)
1.4 rillig 73: pline.CheckTrailingWhitespace()
74: }
1.54 rillig 75: ck.checkOmf(plines)
1.34 rillig 76: CheckLinesTrailingEmptyLines(plainLines)
1.1 rillig 77:
1.37 rillig 78: sorter := NewPlistLineSorter(plines)
79: sorter.Sort()
80: if !sorter.autofixed {
1.4 rillig 81: SaveAutofixChanges(plainLines)
82: }
83: }
1.1 rillig 84:
1.48 rillig 85: func (*PlistChecker) newLines(lines *Lines) []*PlistLine {
1.31 rillig 86: plines := make([]*PlistLine, lines.Len())
87: for i, line := range lines.Lines {
1.36 rillig 88: var conditions []string
89: text := line.Text
90:
91: for hasPrefix(text, "${PLIST.") /* just for performance */ {
92: if m, cond, rest := match2(text, `^(?:\$\{(PLIST\.[\w-.]+)\})(.*)`); m {
93: conditions = append(conditions, cond)
94: text = rest
95: } else {
96: break
1.1 rillig 97: }
98: }
1.36 rillig 99:
100: plines[i] = &PlistLine{line, conditions, text}
1.4 rillig 101: }
102: return plines
103: }
1.1 rillig 104:
1.33 rillig 105: var plistLineStart = textproc.NewByteSet("$0-9A-Za-z")
106:
1.4 rillig 107: func (ck *PlistChecker) collectFilesAndDirs(plines []*PlistLine) {
1.36 rillig 108:
1.4 rillig 109: for _, pline := range plines {
1.48 rillig 110: text := pline.text
111: switch {
112: case text == "":
113: break
114: case plistLineStart.Contains(text[0]):
115: ck.collectPath(NewRelPathString(text), pline)
116: case text[0] == '@':
117: ck.collectDirective(pline)
1.1 rillig 118: }
119: }
120: }
121:
1.48 rillig 122: func (ck *PlistChecker) collectPath(rel RelPath, pline *PlistLine) {
123:
124: // TODO: What about paths containing variables?
125: // Are they intended to be collected as well?
126:
127: if prev := ck.allFiles[rel]; prev == nil || stringSliceLess(pline.conditions, prev.conditions) {
128: ck.allFiles[rel] = pline
129: }
1.53 rillig 130: for dir := rel.Dir(); dir != "."; dir = dir.Dir() {
1.48 rillig 131: ck.allDirs[dir] = pline
132: }
133: }
134:
135: func (ck *PlistChecker) collectDirective(pline *PlistLine) {
136: m, dirname := match1(pline.text, `^@exec \$\{MKDIR\} %D/(.*)$`)
137: if !m || NewPath(dirname).IsAbs() {
138: return
139: }
1.53 rillig 140: for dir := NewRelPathString(dirname); dir != "."; dir = dir.Dir() {
1.48 rillig 141: ck.allDirs[dir] = pline
142: }
143: }
144:
1.35 rillig 145: func (ck *PlistChecker) checkLine(pline *PlistLine) {
1.4 rillig 146: text := pline.text
1.35 rillig 147:
148: if text == "" {
1.28 rillig 149: fix := pline.Autofix()
1.19 rillig 150: fix.Warnf("PLISTs should not contain empty lines.")
151: fix.Delete()
152: fix.Apply()
1.35 rillig 153:
1.48 rillig 154: } else if plistLineStart.Contains(text[0]) {
155: ck.checkPath(pline, pline.Path())
1.35 rillig 156:
157: } else if m, cmd, arg := match2(text, `^@([a-z-]+)[\t ]*(.*)`); m {
158: pline.CheckDirective(cmd, arg)
1.53 rillig 159: if cmd == "comment" && pline.Location.lineno > 1 {
1.48 rillig 160: ck.nonAsciiAllowed = true
161: }
1.35 rillig 162:
1.1 rillig 163: } else {
1.48 rillig 164: pline.Errorf("Invalid line type: %s", pline.Line.Text)
1.1 rillig 165: }
166: }
167:
1.48 rillig 168: func (ck *PlistChecker) checkPath(pline *PlistLine, rel RelPath) {
1.39 rillig 169: ck.checkPathNonAscii(pline)
1.4 rillig 170: ck.checkSorted(pline)
1.21 rillig 171: ck.checkDuplicate(pline)
1.1 rillig 172:
1.48 rillig 173: if contains(rel.Base(), "${IMAKE_MANNEWSUFFIX}") {
1.4 rillig 174: pline.warnImakeMannewsuffix()
1.1 rillig 175: }
1.35 rillig 176:
1.48 rillig 177: if rel.HasPrefixPath("${PKGMANDIR}") {
1.28 rillig 178: fix := pline.Autofix()
1.35 rillig 179: fix.Notef("PLIST files should use \"man/\" instead of \"${PKGMANDIR}\".")
1.19 rillig 180: fix.Explain(
181: "The pkgsrc infrastructure takes care of replacing the correct value",
182: "when generating the actual PLIST for the package.")
183: fix.Replace("${PKGMANDIR}/", "man/")
184: fix.Apply()
185:
1.35 rillig 186: // Since the autofix only applies to the Line, the PlistLine needs to be updated manually.
1.19 rillig 187: pline.text = strings.Replace(pline.text, "${PKGMANDIR}/", "man/", 1)
1.9 rillig 188: }
1.1 rillig 189:
1.48 rillig 190: topdir := rel.Parts()[0]
1.4 rillig 191:
192: switch topdir {
193: case "bin":
1.48 rillig 194: ck.checkPathBin(pline, rel)
1.4 rillig 195: case "doc":
1.28 rillig 196: pline.Errorf("Documentation must be installed under share/doc, not doc.")
1.4 rillig 197: case "etc":
1.48 rillig 198: ck.checkPathEtc(pline)
1.4 rillig 199: case "info":
1.48 rillig 200: ck.checkPathInfo(pline)
1.4 rillig 201: case "lib":
1.48 rillig 202: ck.checkPathLib(pline, rel)
1.4 rillig 203: case "man":
1.35 rillig 204: ck.checkPathMan(pline)
1.4 rillig 205: case "share":
1.35 rillig 206: ck.checkPathShare(pline)
1.4 rillig 207: }
1.1 rillig 208:
1.48 rillig 209: ck.checkPathMisc(rel, pline)
1.52 rillig 210: ck.checkPathCond(pline)
1.48 rillig 211: }
212:
213: func (ck *PlistChecker) checkPathMisc(rel RelPath, pline *PlistLine) {
214: if rel.ContainsText("${PKGLOCALEDIR}") && ck.pkg != nil && !ck.pkg.vars.IsDefined("USE_PKGLOCALEDIR") {
1.35 rillig 215: pline.Warnf("PLIST contains ${PKGLOCALEDIR}, but USE_PKGLOCALEDIR is not set in the package Makefile.")
1.1 rillig 216: }
217:
1.48 rillig 218: if rel.ContainsPath("CVS") {
1.28 rillig 219: pline.Warnf("CVS files should not be in the PLIST.")
1.4 rillig 220: }
1.48 rillig 221: if rel.HasSuffixText(".orig") {
1.28 rillig 222: pline.Warnf(".orig files should not be in the PLIST.")
1.4 rillig 223: }
1.48 rillig 224: if rel.HasBase("perllocal.pod") {
1.37 rillig 225: pline.Warnf("The perllocal.pod file should not be in the PLIST.")
1.40 rillig 226: pline.Explain(
1.35 rillig 227: "This file is handled automatically by the INSTALL/DEINSTALL scripts",
228: "since its contents depends on more than one package.")
1.4 rillig 229: }
1.48 rillig 230: if rel.ContainsText(".egg-info/") {
1.28 rillig 231: pline.Warnf("Include \"../../lang/python/egg.mk\" instead of listing .egg-info files directly.")
1.10 rillig 232: }
1.48 rillig 233: if rel.ContainsPath("..") {
234: pline.Errorf("Paths in PLIST files must not contain \"..\".")
235: } else if canonical := rel.Clean(); canonical != rel {
236: pline.Errorf("Paths in PLIST files must be canonical (%s).", canonical)
237: }
1.4 rillig 238: }
1.1 rillig 239:
1.39 rillig 240: func (ck *PlistChecker) checkPathNonAscii(pline *PlistLine) {
241: text := pline.text
242:
243: lex := textproc.NewLexer(text)
1.51 rillig 244: lex.SkipBytesFunc(func(b byte) bool { return b >= ' ' && b <= '~' })
1.39 rillig 245: ascii := lex.EOF()
246:
247: switch {
248: case !ck.nonAsciiAllowed && !ascii:
249: ck.nonAsciiAllowed = true
250:
251: pline.Warnf("Non-ASCII filename %q.", escapePrintable(text))
252: pline.Explain(
253: "The great majority of filenames installed by pkgsrc packages",
254: "are ASCII-only. Filenames containing non-ASCII characters",
255: "can cause various problems since their name may already be",
256: "different when another character encoding is set in the locale.",
257: "",
258: "To mark a filename as intentionally non-ASCII, insert a PLIST",
259: "@comment with a convincing reason directly above this line.",
260: "That comment will allow this line and the lines directly",
261: "below it to contain non-ASCII filenames.")
262:
263: case ck.nonAsciiAllowed && ascii:
264: ck.nonAsciiAllowed = false
265: }
266: }
267:
1.4 rillig 268: func (ck *PlistChecker) checkSorted(pline *PlistLine) {
1.48 rillig 269: if !pline.HasPlainPath() {
270: return
271: }
272:
273: rel := pline.Path()
274: if ck.lastFname != "" && ck.lastFname > rel && !G.Logger.Opts.Autofix {
275: pline.Warnf("%q should be sorted before %q.", rel.String(), ck.lastFname.String())
276: pline.Explain(
277: "The files in the PLIST should be sorted alphabetically.",
278: "This allows human readers to quickly see whether a file is included or not.")
1.4 rillig 279: }
1.48 rillig 280: ck.lastFname = rel
1.4 rillig 281: }
1.1 rillig 282:
1.21 rillig 283: func (ck *PlistChecker) checkDuplicate(pline *PlistLine) {
1.48 rillig 284: if !pline.HasPlainPath() {
1.21 rillig 285: return
286: }
287:
1.48 rillig 288: prev := ck.allFiles[pline.Path()]
1.36 rillig 289: if prev == pline || len(prev.conditions) > 0 {
1.21 rillig 290: return
291: }
292:
1.28 rillig 293: fix := pline.Autofix()
1.48 rillig 294: fix.Errorf("Duplicate filename %q, already appeared in %s.", pline.text, pline.RelLine(prev.Line))
1.21 rillig 295: fix.Delete()
296: fix.Apply()
297: }
298:
1.48 rillig 299: func (ck *PlistChecker) checkPathBin(pline *PlistLine, rel RelPath) {
300: if rel.Count() > 2 {
1.28 rillig 301: pline.Warnf("The bin/ directory should not have subdirectories.")
1.40 rillig 302: pline.Explain(
1.26 rillig 303: "The programs in bin/ are collected there to be executable by the",
1.33 rillig 304: "user without having to type an absolute path.",
305: "This advantage does not apply to programs in subdirectories of bin/.",
306: "These programs should rather be placed in libexec/PKGBASE.")
1.4 rillig 307: return
308: }
1.1 rillig 309: }
310:
1.48 rillig 311: func (ck *PlistChecker) checkPathEtc(pline *PlistLine) {
1.4 rillig 312: if hasPrefix(pline.text, "etc/rc.d/") {
1.35 rillig 313: pline.Errorf("RCD_SCRIPTS must not be registered in the PLIST.")
314: pline.Explain(
315: "Please use the RCD_SCRIPTS framework, which is described in mk/pkginstall/bsd.pkginstall.mk.")
1.4 rillig 316: return
317: }
318:
1.35 rillig 319: pline.Errorf("Configuration files must not be registered in the PLIST.")
320: pline.Explain(
1.4 rillig 321: "Please use the CONF_FILES framework, which is described in mk/pkginstall/bsd.pkginstall.mk.")
322: }
1.1 rillig 323:
1.48 rillig 324: func (ck *PlistChecker) checkPathInfo(pline *PlistLine) {
1.4 rillig 325: if pline.text == "info/dir" {
1.28 rillig 326: pline.Errorf("\"info/dir\" must not be listed. Use install-info to add/remove an entry.")
1.4 rillig 327: return
328: }
1.1 rillig 329:
1.44 rillig 330: if ck.pkg != nil && !ck.pkg.vars.IsDefined("INFO_FILES") {
1.28 rillig 331: pline.Warnf("Packages that install info files should set INFO_FILES in the Makefile.")
1.1 rillig 332: }
1.4 rillig 333: }
1.1 rillig 334:
1.48 rillig 335: func (ck *PlistChecker) checkPathLib(pline *PlistLine, rel RelPath) {
1.39 rillig 336:
1.1 rillig 337: switch {
1.8 rillig 338:
1.48 rillig 339: case rel.HasPrefixPath("lib/locale"):
1.28 rillig 340: pline.Errorf("\"lib/locale\" must not be listed. Use ${PKGLOCALEDIR}/locale and set USE_PKGLOCALEDIR instead.")
1.4 rillig 341: return
342: }
1.1 rillig 343:
1.48 rillig 344: basename := rel.Base()
1.4 rillig 345: if contains(basename, ".a") || contains(basename, ".so") {
1.48 rillig 346: la := replaceAll(pline.text, `(\.a|\.so[0-9.]*)$`, ".la")
347: if la != pline.text {
348: laLine := ck.allFiles[NewRelPathString(la)]
349: if laLine != nil {
350: pline.Warnf("Redundant library found. The libtool library is in %s.",
351: pline.RelLine(laLine.Line))
1.4 rillig 352: }
353: }
354: }
1.42 rillig 355:
356: pkg := ck.pkg
357: if pkg == nil {
358: return
359: }
360:
361: if pline.text == "lib/charset.alias" && pkg.Pkgpath != "converters/libiconv" {
362: pline.Errorf("Only the libiconv package may install lib/charset.alias.")
363: }
364:
1.44 rillig 365: if hasSuffix(basename, ".la") && !pkg.vars.IsDefined("USE_LIBTOOL") {
1.42 rillig 366: if ck.once.FirstTime("USE_LIBTOOL") {
367: pline.Warnf("Packages that install libtool libraries should define USE_LIBTOOL.")
368: }
369: }
1.4 rillig 370: }
1.1 rillig 371:
1.35 rillig 372: func (ck *PlistChecker) checkPathMan(pline *PlistLine) {
1.57 rillig 373: m, catOrMan, section, base := match3(pline.text, `^man/(cat|man)(\w+)/(.*)$`)
374: if !m {
375: // maybe: line.Warnf("Invalid filename %q for manual page.", text)
376: return
377: }
378: m, manpage, ext, gz := match3(base, `^(.*?)\.(\w+)(\.gz)?$`)
1.4 rillig 379: if !m {
1.32 rillig 380: // maybe: line.Warnf("Invalid filename %q for manual page.", text)
1.4 rillig 381: return
382: }
383:
1.28 rillig 384: if !matches(section, `^[0-9ln]$`) {
385: pline.Warnf("Unknown section %q for manual page.", section)
1.4 rillig 386: }
1.1 rillig 387:
1.47 rillig 388: if catOrMan == "cat" && ck.allFiles[NewRelPathString("man/man"+section+"/"+manpage+"."+section)] == nil {
1.28 rillig 389: pline.Warnf("Preformatted manual page without unformatted one.")
1.4 rillig 390: }
1.1 rillig 391:
1.4 rillig 392: if catOrMan == "cat" {
393: if ext != "0" {
1.28 rillig 394: pline.Warnf("Preformatted manual pages should end in \".0\".")
1.1 rillig 395: }
1.4 rillig 396: } else {
397: if !hasPrefix(ext, section) {
1.28 rillig 398: pline.Warnf("Mismatch between the section (%s) and extension (%s) of the manual page.", section, ext)
1.4 rillig 399: }
400: }
1.1 rillig 401:
1.19 rillig 402: if gz != "" {
1.28 rillig 403: fix := pline.Autofix()
1.19 rillig 404: fix.Notef("The .gz extension is unnecessary for manual pages.")
405: fix.Explain(
1.4 rillig 406: "Whether the manual pages are installed in compressed form or not is",
1.33 rillig 407: "configured by the pkgsrc user.",
408: "Compression and decompression takes place automatically,",
409: "no matter if the .gz extension is mentioned in the PLIST or not.")
1.50 rillig 410: fix.ReplaceAt(0, len(pline.Text)-len(".gz"), ".gz", "")
1.19 rillig 411: fix.Apply()
1.4 rillig 412: }
413: }
1.1 rillig 414:
1.35 rillig 415: func (ck *PlistChecker) checkPathShare(pline *PlistLine) {
1.28 rillig 416: text := pline.text
1.39 rillig 417:
1.4 rillig 418: switch {
1.52 rillig 419: case ck.pkg != nil && hasPrefix(text, "share/icons/"):
1.42 rillig 420: ck.checkPathShareIcons(pline)
1.1 rillig 421:
422: case hasPrefix(text, "share/doc/html/"):
1.37 rillig 423: pline.Warnf("Use of \"share/doc/html\" is deprecated. Use \"share/doc/${PKGBASE}\" instead.")
1.1 rillig 424:
425: case hasPrefix(text, "share/info/"):
1.28 rillig 426: pline.Warnf("Info pages should be installed into info/, not share/info/.")
1.40 rillig 427: pline.Explain(
1.28 rillig 428: "To fix this, add INFO_FILES=yes to the package Makefile.")
1.1 rillig 429:
430: case hasPrefix(text, "share/man/"):
1.28 rillig 431: pline.Warnf("Man pages should be installed into man/, not share/man/.")
1.1 rillig 432: }
1.3 rillig 433: }
434:
1.42 rillig 435: func (ck *PlistChecker) checkPathShareIcons(pline *PlistLine) {
436: pkg := ck.pkg
437: text := pline.text
438:
439: if hasPrefix(text, "share/icons/hicolor/") && pkg.Pkgpath != "graphics/hicolor-icon-theme" {
440: f := "../../graphics/hicolor-icon-theme/buildlink3.mk"
441: if !pkg.included.Seen(f) && ck.once.FirstTime("hicolor-icon-theme") {
442: pline.Errorf("Packages that install hicolor icons must include %q in the Makefile.", f)
443: }
444: }
445:
446: if text == "share/icons/hicolor/icon-theme.cache" && pkg.Pkgpath != "graphics/hicolor-icon-theme" {
447: pline.Errorf("The file icon-theme.cache must not appear in any PLIST file.")
448: pline.Explain(
449: "Remove this line and add the following line to the package Makefile.",
450: "",
451: ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"")
452: }
453:
454: if hasPrefix(text, "share/icons/gnome") && pkg.Pkgpath != "graphics/gnome-icon-theme" {
455: f := "../../graphics/gnome-icon-theme/buildlink3.mk"
456: if !pkg.included.Seen(f) {
457: pline.Errorf("The package Makefile must include %q.", f)
458: pline.Explain(
459: "Packages that install GNOME icons must maintain the icon theme",
460: "cache.")
461: }
462: }
463:
1.44 rillig 464: if contains(text[12:], "/") && !pkg.vars.IsDefined("ICON_THEMES") && ck.once.FirstTime("ICON_THEMES") {
1.42 rillig 465: pline.Warnf("Packages that install icon theme files should set ICON_THEMES.")
466: }
467: }
468:
1.52 rillig 469: func (ck *PlistChecker) checkPathCond(pline *PlistLine) {
470: if ck.pkg == nil {
471: return
472: }
473:
474: for _, cond := range pline.conditions {
475: ck.checkCond(pline, cond[6:])
476: }
477: }
478:
479: func (ck *PlistChecker) checkCond(pline *PlistLine, cond string) {
480: vars := ck.pkg.vars
481: mkline := vars.LastDefinition("PLIST_VARS")
482: if mkline == nil || ck.once.SeenSlice("cond", cond) {
483: return
484: }
485:
486: plistVars := vars.LastValue("PLIST_VARS")
487: resolvedPlistVars := resolveVariableRefs(plistVars, nil, ck.pkg)
488: for _, varparam := range mkline.ValueFields(resolvedPlistVars) {
489: if varparam == cond {
490: return
491: }
492: if containsVarUse(varparam) {
493: trace.Stepf(
494: "Skipping check for condition %q because PLIST_VARS "+
495: "contains the unresolved %q as part of %q.",
496: cond, varparam, resolvedPlistVars)
497: return
498: }
499: }
500:
501: assert(ck.once.FirstTimeSlice("cond", cond))
502: pline.Warnf(
503: "Condition %q should be added to PLIST_VARS in the package Makefile.",
504: cond)
505: }
506:
1.54 rillig 507: func (ck *PlistChecker) checkOmf(plines []*PlistLine) {
508: if ck.pkg == nil {
509: return
510: }
511: mkline := ck.pkg.Includes("../../mk/omf-scrollkeeper.mk")
512: if mkline == nil {
513: return
514: }
515:
516: for _, pline := range plines {
517: if hasSuffix(pline.text, ".omf") {
518: return
519: }
520: }
521:
522: fix := mkline.Autofix()
523: fix.Errorf("Only packages that have .omf files in their PLIST may include omf-scrollkeeper.mk.")
524: if !mkline.HasRationale() {
525: fix.Delete()
526: }
527: fix.Apply()
528: }
529:
1.44 rillig 530: type PlistLine struct {
531: *Line
1.58 ! rillig 532: // XXX: Why "PLIST.docs" and not simply "docs"?
1.44 rillig 533: conditions []string // e.g. PLIST.docs
534: text string // Line.Text without any conditions of the form ${PLIST.cond}
535: }
536:
1.55 rillig 537: func (pline *PlistLine) HasPath() bool {
538: return pline.text != "" && plistLineStart.Contains(pline.text[0])
539: }
1.48 rillig 540:
541: func (pline *PlistLine) HasPlainPath() bool {
542: text := pline.text
543: return text != "" &&
544: plistLineStart.Contains(text[0]) &&
1.51 rillig 545: !containsVarUse(text)
1.48 rillig 546: }
547:
1.55 rillig 548: func (pline *PlistLine) Path() RelPath {
549: assert(pline.HasPath())
550: return NewRelPathString(pline.text)
551: }
552:
1.4 rillig 553: func (pline *PlistLine) CheckTrailingWhitespace() {
554: if hasSuffix(pline.text, " ") || hasSuffix(pline.text, "\t") {
1.37 rillig 555: pline.Errorf("Pkgsrc does not support filenames ending in whitespace.")
1.40 rillig 556: pline.Explain(
1.32 rillig 557: "Each character in the PLIST is relevant, even trailing whitespace.")
1.3 rillig 558: }
559: }
560:
1.4 rillig 561: func (pline *PlistLine) CheckDirective(cmd, arg string) {
562: if cmd == "unexec" {
1.28 rillig 563: if m, dir := match1(arg, `^(?:rmdir|\$\{RMDIR\} %D/)(.*)`); m {
1.25 rillig 564: if !contains(dir, "true") && !contains(dir, "${TRUE}") {
1.28 rillig 565: fix := pline.Autofix()
566: fix.Warnf("Please remove this line. It is no longer necessary.")
567: fix.Delete()
568: fix.Apply()
1.3 rillig 569: }
570: }
571: }
1.1 rillig 572:
1.4 rillig 573: switch cmd {
574: case "exec", "unexec":
575: switch {
576: case contains(arg, "ldconfig") && !contains(arg, "/usr/bin/true"):
1.37 rillig 577: pline.Errorf("The ldconfig command must be used with \"||/usr/bin/true\".")
1.1 rillig 578: }
579:
1.4 rillig 580: case "comment":
1.35 rillig 581: // Nothing to check.
1.3 rillig 582:
1.4 rillig 583: case "dirrm":
1.28 rillig 584: pline.Warnf("@dirrm is obsolete. Please remove this line.")
1.40 rillig 585: pline.Explain(
1.4 rillig 586: "Directories are removed automatically when they are empty.",
587: "When a package needs an empty directory, it can use the @pkgdir",
1.32 rillig 588: "command in the PLIST.")
1.3 rillig 589:
1.4 rillig 590: case "imake-man":
1.34 rillig 591: args := strings.Fields(arg)
1.4 rillig 592: switch {
593: case len(args) != 3:
1.34 rillig 594: pline.Warnf("Invalid number of arguments for imake-man, should be 3.")
1.4 rillig 595: case args[2] == "${IMAKE_MANNEWSUFFIX}":
596: pline.warnImakeMannewsuffix()
1.3 rillig 597: }
598:
1.4 rillig 599: case "pkgdir":
600: // Nothing to check.
1.3 rillig 601:
1.4 rillig 602: default:
1.28 rillig 603: pline.Warnf("Unknown PLIST directive \"@%s\".", cmd)
1.3 rillig 604: }
605: }
606:
1.4 rillig 607: func (pline *PlistLine) warnImakeMannewsuffix() {
1.28 rillig 608: pline.Warnf("IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs.")
1.40 rillig 609: pline.Explain(
1.1 rillig 610: "This is the result of a print-PLIST call that has not been edited",
1.33 rillig 611: "manually by the package maintainer.",
612: "Please replace the IMAKE_MANNEWSUFFIX with:",
1.1 rillig 613: "",
614: "\tIMAKE_MAN_SUFFIX for programs,",
615: "\tIMAKE_LIBMAN_SUFFIX for library functions,",
616: "\tIMAKE_FILEMAN_SUFFIX for file formats,",
617: "\tIMAKE_GAMEMAN_SUFFIX for games,",
618: "\tIMAKE_MISCMAN_SUFFIX for other man pages.")
619: }
1.4 rillig 620:
621: type plistLineSorter struct {
1.16 rillig 622: header []*PlistLine // Does not take part in sorting
623: middle []*PlistLine // Only this part is sorted
624: footer []*PlistLine // Does not take part in sorting, typically contains @exec or @pkgdir
1.42 rillig 625: unsortable *Line // Some lines are so difficult to sort that only humans can do that
1.16 rillig 626: changed bool // Whether the sorting actually changed something
627: autofixed bool // Whether the newly sorted file has been written to disk
1.4 rillig 628: }
629:
630: func NewPlistLineSorter(plines []*PlistLine) *plistLineSorter {
1.16 rillig 631: headerEnd := 0
632: for headerEnd < len(plines) && hasPrefix(plines[headerEnd].text, "@comment") {
633: headerEnd++
634: }
635:
636: footerStart := len(plines)
637: for footerStart > headerEnd && hasPrefix(plines[footerStart-1].text, "@") {
638: footerStart--
639: }
640:
641: header := plines[0:headerEnd]
642: middle := plines[headerEnd:footerStart]
643: footer := plines[footerStart:]
1.42 rillig 644: var unsortable *Line
1.16 rillig 645:
646: for _, pline := range middle {
647: if unsortable == nil && (hasPrefix(pline.text, "@") || contains(pline.text, "$")) {
1.28 rillig 648: unsortable = pline.Line
1.4 rillig 649: }
650: }
1.16 rillig 651: return &plistLineSorter{header, middle, footer, unsortable, false, false}
1.4 rillig 652: }
653:
654: func (s *plistLineSorter) Sort() {
1.16 rillig 655: if line := s.unsortable; line != nil {
1.50 rillig 656: if trace.Tracing {
1.28 rillig 657: trace.Stepf("%s: This line prevents pkglint from sorting the PLIST automatically.", line)
1.16 rillig 658: }
659: return
660: }
661:
1.41 rillig 662: if !G.Logger.shallBeLogged("%q should be sorted before %q.") {
1.24 rillig 663: return
664: }
1.16 rillig 665: if len(s.middle) == 0 {
666: return
667: }
1.28 rillig 668: firstLine := s.middle[0].Line
1.21 rillig 669:
670: sort.SliceStable(s.middle, func(i, j int) bool {
671: mi := s.middle[i]
672: mj := s.middle[j]
1.36 rillig 673: less := mi.text < mj.text ||
1.46 rillig 674: mi.text == mj.text && stringSliceLess(mi.conditions, mj.conditions)
675: if i < j != less {
1.21 rillig 676: s.changed = true
677: }
678: return less
679: })
1.4 rillig 680:
1.16 rillig 681: if !s.changed {
1.4 rillig 682: return
683: }
684:
1.19 rillig 685: fix := firstLine.Autofix()
1.31 rillig 686: fix.Notef(SilentAutofixFormat)
1.53 rillig 687: fix.Describef(0, "Sorting the whole file.")
1.19 rillig 688: fix.Apply()
689:
1.42 rillig 690: var lines []*Line
1.16 rillig 691: for _, pline := range s.header {
1.28 rillig 692: lines = append(lines, pline.Line)
1.16 rillig 693: }
694: for _, pline := range s.middle {
1.28 rillig 695: lines = append(lines, pline.Line)
1.16 rillig 696: }
697: for _, pline := range s.footer {
1.28 rillig 698: lines = append(lines, pline.Line)
1.4 rillig 699: }
1.19 rillig 700:
1.53 rillig 701: s.autofixed = SaveAutofixChanges(NewLines(lines[0].Filename(), lines))
1.4 rillig 702: }
1.55 rillig 703:
1.56 rillig 704: type PlistRank struct {
705: Rank int
706: Opsys string
707: Arch string
708: Rest string
709: }
1.55 rillig 710:
1.56 rillig 711: var defaultPlistRank = &PlistRank{0, "", "", ""}
712:
713: func NewPlistRank(basename string) *PlistRank {
714: isOpsys := func(s string) bool {
715: return G.Pkgsrc.VariableType(nil, "OPSYS").basicType.HasEnum(s)
716: }
717: isArch := func(s string) bool {
718: return G.Pkgsrc.VariableType(nil, "MACHINE_ARCH").basicType.HasEnum(s)
719: }
720: isEmulOpsys := func(s string) bool {
721: return G.Pkgsrc.VariableType(nil, "EMUL_OPSYS").basicType.HasEnum(s)
722: }
723: isEmulArch := func(s string) bool {
724: return G.Pkgsrc.VariableType(nil, "EMUL_ARCH").basicType.HasEnum(s)
725: }
726:
727: switch basename {
728: case "PLIST":
729: return defaultPlistRank
730: case "PLIST.common":
731: return &PlistRank{1, "", "", ""}
732: case "PLIST.common_end":
733: return &PlistRank{2, "", "", ""}
734: }
735:
736: parts := strings.Split(basename[6:], "-")
737: rank := PlistRank{3, "", "", ""}
738: if isOpsys(parts[0]) {
739: rank.Opsys = parts[0]
740: parts = parts[1:]
741: }
742: if len(parts) > 0 && isArch(parts[0]) {
743: rank.Arch = parts[0]
744: parts = parts[1:]
745: }
746: if len(parts) >= 2 && isEmulOpsys(parts[0]) && isEmulArch(parts[1]) {
747: rank.Opsys = parts[0]
748: rank.Arch = parts[1]
749: parts = parts[2:]
750: }
751: rank.Rest = strings.Join(parts, "-")
752: return &rank
753: }
1.55 rillig 754:
755: // The ranks among the files are:
756: // PLIST
757: // -> PLIST.common
758: // -> PLIST.common_end
759: // -> { PLIST.OPSYS, PLIST.ARCH }
760: // -> { PLIST.OPSYS.ARCH, PLIST.EMUL_PLATFORM }
761: // Files are a later level must not mention files that are already
762: // mentioned at an earlier level.
1.56 rillig 763: func (r *PlistRank) MoreGeneric(other *PlistRank) bool {
764: if r.Rank != 3 && other.Rank != 3 {
765: return r.Rank < other.Rank
766: }
767: if r.Opsys != "" && r.Opsys != other.Opsys {
768: return false
769: }
770: if r.Arch != "" && r.Arch != other.Arch {
771: return false
772: }
773: if r.Rest != "" && r.Rest != other.Rest {
774: return false
775: }
776: return *r != *other
1.55 rillig 777: }
778:
779: type PlistLines struct {
780: all map[RelPath][]*plistLineData
781: }
782:
783: func NewPlistLines() *PlistLines {
784: return &PlistLines{make(map[RelPath][]*plistLineData)}
785: }
786:
787: type plistLineData struct {
788: line *PlistLine
1.56 rillig 789: rank *PlistRank
1.55 rillig 790: }
791:
1.56 rillig 792: func (pl *PlistLines) Add(line *PlistLine, rank *PlistRank) {
1.55 rillig 793: path := line.Path()
794: for _, existing := range pl.all[path] {
1.56 rillig 795: switch {
796: case existing.rank == rank:
797: break
798: case existing.rank.MoreGeneric(rank):
1.55 rillig 799: line.Errorf("Path %s is already listed in %s.",
800: path, line.RelLine(existing.line.Line))
1.56 rillig 801: case rank.MoreGeneric(existing.rank):
802: existing.line.Errorf("Path %s is already listed in %s.",
803: path, existing.line.RelLine(line.Line))
1.55 rillig 804: }
805: }
806: pl.all[path] = append(pl.all[path], &plistLineData{line, rank})
807: }
CVSweb <webmaster@jp.NetBSD.org>