source: trunk/base/src/port1.0/portlint.tcl @ 89428

Last change on this file since 89428 was 89428, checked in by raimue@…, 9 years ago

port1.0/portlint.tcl:
Check for incorrect svn properties

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 21.2 KB
Line 
1# et:ts=4
2# portlint.tcl
3# $Id: portlint.tcl 89428 2012-01-28 19:42:22Z raimue@macports.org $
4
5package provide portlint 1.0
6package require portutil 1.0
7
8set org.macports.lint [target_new org.macports.lint portlint::lint_main]
9target_runtype ${org.macports.lint} always
10target_state ${org.macports.lint} no
11target_provides ${org.macports.lint} lint
12target_requires ${org.macports.lint} main
13target_prerun ${org.macports.lint} portlint::lint_start
14
15namespace eval portlint {
16}
17
18set_ui_prefix
19
20set lint_portsystem \
21    "1.0"
22
23set lint_platforms [list \
24    "darwin" \
25    "freebsd" \
26    "linux" \
27    "macosx" \
28    "netbsd" \
29    "openbsd" \
30    "puredarwin" \
31    "solaris" \
32    "sunos" \
33    ]
34
35set lint_required [list \
36    "name" \
37    "version" \
38    "description" \
39    "long_description" \
40    "categories" \
41    "maintainers" \
42    "platforms" \
43    "homepage" \
44    "master_sites" \
45    "checksums" \
46    "license"
47    ]
48
49set lint_optional [list \
50    "epoch" \
51    "revision" \
52    "worksrcdir" \
53    "distname" \
54    "use_automake" \
55    "use_autoconf" \
56    "use_configure" \
57    ]
58
59proc portlint::seems_utf8 {str} {
60    set len [string length $str]
61    for {set i 0} {$i<$len} {incr i} {
62        set c [scan [string index $str $i] %c]
63        if {$c < 0x80} {
64            # ASCII
65            continue
66        } elseif {($c & 0xE0) == 0xC0} {
67            set n 1
68        } elseif {($c & 0xF0) == 0xE0} {
69            set n 2
70        } elseif {($c & 0xF8) == 0xF0} {
71            set n 3
72        } elseif {($c & 0xFC) == 0xF8} {
73            set n 4
74        } elseif {($c & 0xFE) == 0xFC} {
75            set n 5
76        } else {
77            return false
78        }
79        for {set j 0} {$j<$n} {incr j} {
80            incr i
81            if {$i == $len} {
82                return false
83            } elseif {([scan [string index $str $i] %c] & 0xC0) != 0x80} {
84                return false
85            }
86        }
87    }
88    return true
89}
90
91
92proc portlint::lint_start {args} {
93    global UI_PREFIX subport
94    ui_notice "$UI_PREFIX [format [msgcat::mc "Verifying Portfile for %s"] ${subport}]"
95}
96
97proc portlint::lint_main {args} {
98    global UI_PREFIX name portpath porturl ports_lint_nitpick
99    set portfile ${portpath}/Portfile
100    set portdirs [split ${portpath} /]
101    set last [llength $portdirs]
102    incr last -1
103    set portdir [lindex $portdirs $last]
104    incr last -1
105    set portcatdir [lindex $portdirs $last]
106
107    set warnings 0
108    set errors 0
109
110    ###################################################################
111    ui_debug "$portfile"
112   
113    if {[info exists ports_lint_nitpick] && $ports_lint_nitpick eq "yes"} {
114        set nitpick true
115    } else {
116        set nitpick false
117    }
118
119    set topline_number 1
120    set require_blank false
121    set require_after ""
122    set seen_portsystem false
123    set seen_portgroup false
124    set in_description false
125
126    array set portgroups {}
127
128    set local_variants [list]
129
130    set f [open $portfile RDONLY]
131    # read binary (to check UTF-8)
132    fconfigure $f -encoding binary
133    set lineno 1
134    set hashline false
135    while {1} {
136        set line [gets $f]
137        if {[eof $f]} {
138            if {$nitpick} {
139                seek $f -1 end
140                set last [read $f 1]
141                if {![string match "\n" $last]} {
142                    ui_warn "Line $lineno has missing newline (at end of file)"
143                    incr warnings
144                }
145            }
146            close $f
147            break
148        }
149        ui_debug "$lineno: $line"
150
151        if {![seems_utf8 $line]} {
152            ui_error "Line $lineno seems to contain an invalid UTF-8 sequence"
153            incr errors
154        }
155
156        if {($require_after == "PortSystem" || $require_after == "PortGroup") && \
157            [string match "PortGroup*" $line]} {
158            set require_blank false
159        }
160
161        if {$nitpick && $require_blank && ($line != "")} {
162            ui_warn "Line $lineno should be a newline (after $require_after)"
163            incr warnings
164        }
165        set require_blank false
166
167        if {$nitpick && [regexp {\S[ \t]+$} $line]} {
168            # allow indented blank lines between blocks of code and such
169            ui_warn "Line $lineno has trailing whitespace before newline"
170            incr warnings
171        }
172
173        if {($lineno == $topline_number) && [string match "*-\*- *" $line]} {
174            ui_info "OK: Line $lineno has emacs/vim Mode"
175            incr topline_number
176        }
177        if {($lineno == $topline_number) && ![string match "*\$Id*\$" $line]} {
178            ui_warn "Line $lineno is missing RCS tag (\$Id\$)"
179            incr warnings
180        } elseif {($lineno == $topline_number)} {
181            ui_info "OK: Line $lineno has RCS tag (\$Id\$)"
182            set require_blank true
183            set require_after "RCS tag"
184        }
185       
186        # skip the rest for comment lines (not perfectly accurate...)
187        if {[regexp {^\s*#} $line]} {
188            incr lineno
189            continue
190        }
191
192        if {[string match "PortSystem*" $line]} {
193            if {$seen_portsystem} {
194                ui_error "Line $lineno repeats PortSystem information"
195                incr errors
196            }
197            regexp {PortSystem\s+([0-9.]+)} $line -> portsystem
198            if {![info exists portsystem]} {
199                ui_error "Line $lineno has unrecognized PortSystem"
200                incr errors
201            }
202            set seen_portsystem true
203            set require_blank true
204            set require_after "PortSystem"
205        }
206        if {[string match "PortGroup*" $line]} {
207            regexp {PortGroup\s+([a-z0-9]+)\s+([0-9.]+)} $line -> portgroup portgroupversion
208            if {![info exists portgroup]} {
209                ui_error "Line $lineno has unrecognized PortGroup"
210                incr errors
211            }
212            if {[info exists portgroups($portgroup)]} {
213                ui_error "Line $lineno repeats PortGroup information"
214                incr errors
215            } else {
216                set portgroups($portgroup) $portgroupversion
217            }
218            set seen_portgroup true
219            set require_blank true
220            set require_after "PortGroup"
221        }
222
223        # TODO: check for repeated variable definitions
224        # TODO: check the definition order of variables
225        # TODO: check length of description against max
226
227        if {[string match "long_description*" $line]} {
228            set in_description true
229        }
230        if {$in_description && ([string range $line end end] != "\\")} {
231            set in_description false
232            #set require_blank true
233            #set require_after "long_description"
234        } elseif {$in_description} {
235            set require_blank false
236        }
237
238        if {[string match "variant*" $line]} {
239            regexp {variant\s+(\w+)} $line -> variantname
240            if {[info exists variantname]} {
241                lappend local_variants $variantname
242            }
243        }
244       
245        if {[string match "platform\[ \t\]*" $line]} {
246            regexp {platform\s+(?:\w+\s+(?:\w+\s+)?)?(\w+)} $line -> platform_arch
247            if {$platform_arch == "ppc"} {
248                ui_error "Arch 'ppc' in platform on line $lineno should be 'powerpc'"
249                incr errors
250            }
251        }
252
253        if {[regexp {(^|\s)configure\s+\{\s*\}} $line]} {
254            ui_warn "Line $lineno should say \"use_configure no\" instead of declaring an empty configure phase"
255            incr warnings
256        }
257
258        # Check for hardcoded version numbers
259        if {$nitpick} {
260            # Support for skipping checksums lines
261            if {[regexp {^checksums} $line]} {
262                # We enter a series of one or more lines containing checksums
263                set hashline true
264            }
265   
266            if {!$hashline
267                    && ![regexp {^PortSystem|^PortGroup|^version} $line]
268                    && ![regexp {^[a-z0-9]+\.setup} $line]
269                    && [string first [option version] $line] != -1} {
270                ui_warn "Line $lineno seems to hardcode the version number, consider using \${version} instead"
271                incr warnings
272            }
273   
274            if {$hashline &&
275                ![string match \\\\ [string index $line end]]} {
276                    # if the last character is not a backslash we're done with
277                    # line skipping
278                    set hashline false
279            }
280        }
281           
282        ### TODO: more checks to Portfile syntax
283
284        incr lineno
285    }
286
287    ###################################################################
288
289    global os.platform os.arch os.version
290    global version revision epoch
291    set portarch [get_canonical_archs]
292    global description long_description platforms categories all_variants
293    global maintainers license homepage master_sites checksums patchfiles
294    global depends_fetch depends_extract depends_lib depends_build depends_run distfiles fetch.type
295    global livecheck.type subport name
296   
297    global lint_portsystem lint_platforms
298    global lint_required lint_optional
299
300    if (!$seen_portsystem) {
301        ui_error "Didn't find PortSystem specification"
302        incr errors
303    }  elseif {$portsystem != $lint_portsystem} {
304        ui_error "Unknown PortSystem: $portsystem"
305        incr errors
306    } else {
307        ui_info "OK: Found PortSystem $portsystem"
308    }
309
310    if ($seen_portgroup) {
311        # Using a PortGroup is optional
312        foreach {portgroup portgroupversion} [array get portgroups] {
313            if {![file exists [getportresourcepath $porturl "port1.0/group/${portgroup}-${portgroupversion}.tcl"]]} {
314                ui_error "Unknown PortGroup: $portgroup-$portgroupversion"
315                incr errors
316            } else {
317                ui_info "OK: Found PortGroup $portgroup-$portgroupversion"
318            }
319        }
320    }
321
322    foreach req_var $lint_required {
323
324        if {$req_var == "master_sites"} {
325            if {${fetch.type} != "standard"} {
326                ui_info "OK: $req_var not required for fetch.type ${fetch.type}"
327                continue
328            }
329            if {[llength ${distfiles}] == 0} {
330                ui_info "OK: $req_var not required when there are no distfiles"
331                continue
332            }
333        }
334
335        if {![info exists $req_var]} {
336            ui_error "Missing required variable: $req_var"
337            incr errors
338        } else {
339            ui_info "OK: Found required variable: $req_var"
340        }
341    }
342
343    foreach opt_var $lint_optional {
344        if {[info exists $opt_var]} {
345            # TODO: check whether it was seen (or default)
346            ui_info "OK: Found optional variable: $opt_var"
347        }
348    }
349   
350    if {[info exists name]} {
351        if {[regexp {[^[:alnum:]_.-]} $name]} {
352            ui_error "Port name '$name' contains unsafe characters. Names should only contain alphanumeric characters, underscores, dashes or dots."
353            incr errors
354        }
355    }
356
357    if {[info exists platforms]} {
358        foreach platform $platforms {
359            if {[lsearch -exact $lint_platforms $platform] == -1} {
360                ui_error "Unknown platform: $platform"
361                incr errors
362            } else {
363                ui_info "OK: Found platform: $platform"
364            }
365        }
366    }
367
368    if {[info exists categories]} {
369        if {[llength $categories] > 0} {
370            set category [lindex $categories 0]
371            ui_info "OK: Found primary category: $category"
372        } else {
373            ui_error "Categories list is empty"
374            incr errors
375        }
376    }
377
378    if {![string is integer -strict $epoch]} {
379        ui_error "Port epoch is not numeric:  $epoch"
380        incr errors
381    }
382    if {![string is integer -strict $revision]} {
383        ui_error "Port revision is not numeric: $revision"
384        incr errors
385    }
386
387    set variantnumber 1
388    foreach variant $all_variants {
389        set variantname [ditem_key $variant name] 
390        set variantdesc [lindex [ditem_key $variant description] 0]
391        if {![info exists variantname] || $variantname == ""} {
392            ui_error "Variant number $variantnumber does not have a name"
393            incr errors
394        } else {
395            set name_ok true
396            set desc_ok true
397
398            if {![regexp {^[A-Za-z0-9_]+$} $variantname]} {
399                ui_error "Variant name $variantname is not valid; use \[A-Za-z0-9_\]+ only"
400                incr errors
401                set name_ok false
402            }
403
404            if {![info exists variantdesc] || $variantdesc == ""} {
405                # don't warn about missing descriptions for global variants
406                if {[lsearch -exact $local_variants $variantname] != -1 &&
407                    [variant_desc $porturl $variantname] == ""} {
408                    ui_warn "Variant $variantname does not have a description"
409                    incr warnings
410                    set desc_ok false
411                } elseif {$variantdesc == ""} {
412                    set variantdesc "(pre-defined variant)"
413                }
414            } else {
415                if {[variant_desc $porturl $variantname] != ""} {
416                    ui_warn "Variant $variantname overrides global description"
417                    incr warnings
418                }
419            }
420
421            # Check if conflicting variants actually exist
422            foreach vconflict [ditem_key $variant conflicts] {
423                set exists 0
424                foreach v $all_variants {
425                    if {$vconflict == [ditem_key $v name]} {
426                        set exists 1
427                        break
428                    }
429                }
430                if {!$exists} {
431                    ui_warn "Variant $variantname conflicts with non-existing variant $vconflict"
432                    incr warnings
433                }
434            }
435
436            if {$name_ok} {
437                if {$desc_ok} {
438                    ui_info "OK: Found variant $variantname: $variantdesc"
439                } else {
440                    ui_info "OK: Found variant: $variantname"
441                }
442            }
443        }
444        incr variantnumber
445    }
446
447    set all_depends {}
448    if {[info exists depends_fetch]} { eval "lappend all_depends $depends_fetch" }
449    if {[info exists depends_extract]} { eval "lappend all_depends $depends_extract" }
450    if {[info exists depends_lib]} { eval "lappend all_depends $depends_lib" }
451    if {[info exists depends_build]} { eval "lappend all_depends $depends_build" }
452    if {[info exists depends_run]} { eval "lappend all_depends $depends_run" }
453    foreach depspec $all_depends {
454        set dep [lindex [split $depspec :] end]
455        if {[catch {set res [mport_lookup $dep]} error]} {
456            global errorInfo
457            ui_debug "$errorInfo"
458            continue
459        }
460        if {$res == ""} {
461            ui_error "Unknown dependency: $dep"
462            incr errors
463        } else {
464            ui_info "OK: Found dependency: $dep"
465        }
466    }
467
468    # Check for multiple dependencies
469    foreach deptype {depends_extract depends_lib depends_build depends_run} {
470        if {[info exists $deptype]} {
471            array set depwarned {}
472            foreach depspec [set $deptype] {
473                if {![info exists depwarned($depspec)]
474                        && [llength [lsearch -exact -all [set $deptype] $depspec]] > 1} {
475                    ui_warn "Dependency $depspec specified multiple times in $deptype"
476                    incr warnings
477                    # Report each depspec only once
478                    set depwarned($depspec) yes
479                }
480            }
481        }
482    }
483
484    if {[regexp "^(.+)nomaintainer(@macports.org)?(.+)$" $maintainers] } {
485        ui_error "Using nomaintainer together with other maintainer"
486        incr errors
487    }
488
489    if {[regexp "^openmaintainer(@macports.org)?$" $maintainers] } {
490        ui_error "Using openmaintainer without any other maintainer"
491        incr errors
492    }
493
494    if {[string match "*darwinports@opendarwin.org*" $maintainers]} {
495        ui_warn "Using legacy email address for no/open maintainer"
496        incr warnings
497    }
498
499    if {[string match "*nomaintainer@macports.org*" $maintainers] ||
500        [string match "*openmaintainer@macports.org*" $maintainers]} {
501        ui_warn "Using full email address for no/open maintainer"
502        incr warnings
503    }
504
505    if {$license == "unknown"} {
506        ui_warn "no license set"
507        incr warnings
508    } else {
509
510        # If maintainer set license, it must follow correct format
511
512        set prev ''
513        foreach test [split [string map { \{ '' \} ''} $license] '\ '] {
514            ui_debug "Checking format of license '${test}'"
515
516            # space instead of hyphen
517            if {[string is double -strict $test]} {
518                ui_error "Invalid license '${prev} ${test}': missing hyphen between ${prev} ${test}"
519
520            # missing hyphen
521            } elseif {![string equal -nocase "X11" $test]} {
522                foreach subtest [split $test '-'] {
523                    ui_debug "testing ${subtest}"
524
525                    # license names start with letters: versions and empty strings need not apply
526                    if {[string is alpha -strict [string index $subtest 0]]} {
527
528                        # if the last character of license name is a number or plus sign
529                        # then a hyphen is missing
530                        set license_end [string index $subtest end]
531                        if {[string equal "+" $license_end] || [string is integer -strict $license_end]} {
532                            ui_error "invalid license '${test}': missing hyphen before version"
533                        }
534                    }
535                }
536            }
537
538            # BSD-2 => BSD
539            if {[string equal -nocase "BSD-2" $test]} {
540                ui_error "Invalid license '${test}': use BSD instead"
541            }
542   
543            # BSD-3 => BSD
544            if {[string equal -nocase "BSD-3" $test]} {
545                ui_error "Invalid license '${test}': use BSD instead"
546            }
547   
548            # BSD-4 => BSD-old
549            if {[string equal -nocase "BSD-4" $test]} {
550                ui_error "Invalid license '${test}': use BSD-old instead"
551            }
552   
553            set prev $test
554        }
555
556    }
557
558    if {$subport != $name && ${livecheck.type} != "none"} {
559        ui_warn "livecheck set for subport $subport"
560    }
561
562    # these checks are only valid for ports stored in the regular tree directories
563    if {[info exists category] && $portcatdir != $category} {
564        ui_error "Portfile parent directory $portcatdir does not match primary category $category"
565        incr errors
566    } else {
567        ui_info "OK: Portfile parent directory matches primary category"
568    }
569    if {$portdir != $name} {
570        ui_error "Portfile directory $portdir does not match port name $name"
571        incr errors
572    } else {
573        ui_info "OK: Portfile directory matches port name"
574    }
575
576    if {$nitpick && [info exists patchfiles]} {
577        foreach patchfile $patchfiles {
578            if {![string match "patch-*.diff" $patchfile] && [file exists "$portpath/files/$patchfile"]} {
579                ui_warn "Patchfile $patchfile does not follow the source patch naming policy \"patch-*.diff\""
580                incr warnings
581            }
582        }
583    }
584
585    # Check for use of deprecated options
586    set deprecated_options_name [get_deprecated_options]
587    global $deprecated_options_name
588    foreach option [array names $deprecated_options_name] {
589        set newoption [lindex [set ${deprecated_options_name}($option)] 0]
590        set refcount  [lindex [set ${deprecated_options_name}($option)] 1]
591
592        if {$refcount > 0} {
593            if {$newoption != ""} {
594                ui_warn "Using deprecated option '$option', superseded by '$newoption'"
595            } else {
596                ui_warn "Using deprecated option '$option'"
597            }
598            incr warnings
599        }
600    }
601
602    ### TODO: more checks to Tcl variables/sections
603
604    ui_debug "Name: $name"
605    ui_debug "Epoch: $epoch"
606    ui_debug "Version: $version"
607    ui_debug "Revision: $revision"
608    ui_debug "Archs: $portarch"
609
610    ###################################################################
611
612    set svn_cmd ""
613    catch {set svn_cmd [findBinary svn]}
614    if {$svn_cmd != "" && ([file exists $portpath/.svn] || ![catch {exec $svn_cmd info $portpath > /dev/null 2>@1}])} {
615        ui_debug "Checking svn properties"
616        if [catch {exec $svn_cmd propget svn:keywords $portfile 2>@1} output] {
617            ui_warn "Unable to check for svn:keywords property: $output"
618        } else {
619            ui_debug "Property svn:keywords is \"$output\", should be \"Id\""
620            if {$output != "Id"} {
621                ui_error "Missing subversion property on Portfile, please execute: svn ps svn:keywords Id Portfile"
622                incr errors
623            }
624        }
625        if [catch {exec $svn_cmd propget svn:eol-style $portfile 2>@1} output] {
626            ui_warn "Unable to check for svn:eol-style property: $output"
627        } else {
628            ui_debug "Property svn:eol-style is \"$output\", should be \"native\""
629            if {$output != "native"} {
630                ui_error "Missing subversion property on Portfile, please execute: svn ps svn:eol-tyle native Portfile"
631                incr errors
632            }
633        }
634    }
635
636    ###################################################################
637
638    ui_notice "$UI_PREFIX [format [msgcat::mc "%d errors and %d warnings found."] $errors $warnings]"
639
640    return {$errors > 0}
641}
Note: See TracBrowser for help on using the repository browser.