| 1 | # et:ts=4 |
| 2 | # portlint.tcl |
| 3 | # $Id: portlint.tcl $ |
| 4 | |
| 5 | package provide portlint 1.0 |
| 6 | package require portutil 1.0 |
| 7 | |
| 8 | set org.macports.lint [target_new org.macports.lint lint_main] |
| 9 | target_runtype ${org.macports.lint} always |
| 10 | target_provides ${org.macports.lint} lint |
| 11 | target_requires ${org.macports.lint} main |
| 12 | target_prerun ${org.macports.lint} lint_start |
| 13 | |
| 14 | set_ui_prefix |
| 15 | |
| 16 | set lint_required [list \ |
| 17 | "name" \ |
| 18 | "version" \ |
| 19 | "description" \ |
| 20 | "long_description" \ |
| 21 | "categories" \ |
| 22 | "maintainers" \ |
| 23 | "platforms" \ |
| 24 | "homepage" \ |
| 25 | "master_sites" \ |
| 26 | "checksums" \ |
| 27 | ] |
| 28 | |
| 29 | set lint_optional [list \ |
| 30 | "epoch" \ |
| 31 | "revision" \ |
| 32 | "worksrcdir" \ |
| 33 | "distname" \ |
| 34 | "use_automake" \ |
| 35 | "use_autoconf" \ |
| 36 | "use_configure" \ |
| 37 | ] |
| 38 | |
| 39 | |
| 40 | proc seems_utf8 {str} { |
| 41 | set len [string length $str] |
| 42 | for {set i 0} {$i<$len} {incr i} { |
| 43 | set c [scan [string index $str $i] %c] |
| 44 | if {$c < 0x80} { |
| 45 | # ASCII |
| 46 | continue |
| 47 | } elseif {($c & 0xE0) == 0xC0} { |
| 48 | set n 1 |
| 49 | } elseif {($c & 0xF0) == 0xE0} { |
| 50 | set n 2 |
| 51 | } elseif {($c & 0xF8) == 0xF0} { |
| 52 | set n 3 |
| 53 | } elseif {($c & 0xFC) == 0xF8} { |
| 54 | set n 4 |
| 55 | } elseif {($c & 0xFE) == 0xFC} { |
| 56 | set n 5 |
| 57 | } else { |
| 58 | return false |
| 59 | } |
| 60 | for {set j 0} {$j<$n} {incr j} { |
| 61 | incr i |
| 62 | if {$i == $len} { |
| 63 | return false |
| 64 | } elseif {([scan [string index $str $i] %c] & 0xC0) != 0x80} { |
| 65 | return false |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | return true |
| 70 | } |
| 71 | |
| 72 | |
| 73 | proc lint_start {args} { |
| 74 | global UI_PREFIX portname |
| 75 | ui_msg "$UI_PREFIX [format [msgcat::mc "Verifying Portfile for %s"] ${portname}]" |
| 76 | } |
| 77 | |
| 78 | proc lint_main {args} { |
| 79 | global UI_PREFIX portname portpath |
| 80 | set portfile ${portpath}/Portfile |
| 81 | |
| 82 | set warnings 0 |
| 83 | set errors 0 |
| 84 | |
| 85 | ################################################################### |
| 86 | ui_debug "$portfile" |
| 87 | |
| 88 | set require_blank false |
| 89 | set require_after "" |
| 90 | set seen_portsystem false |
| 91 | set seen_portgroup false |
| 92 | set in_description false |
| 93 | |
| 94 | set f [open $portfile RDONLY] |
| 95 | # read binary (to check UTF-8) |
| 96 | fconfigure $f -encoding binary |
| 97 | set lineno 1 |
| 98 | while {1} { |
| 99 | set line [gets $f] |
| 100 | if {[eof $f]} { |
| 101 | close $f |
| 102 | break |
| 103 | } |
| 104 | ui_debug "$lineno: $line" |
| 105 | |
| 106 | if {![seems_utf8 $line]} { |
| 107 | ui_error "Line $lineno seems to contain an invalid UTF-8 sequence" |
| 108 | incr errors |
| 109 | } |
| 110 | |
| 111 | if {[string equal "PortSystem" $require_after] && \ |
| 112 | [string match "PortGroup*" $line]} { |
| 113 | set require_blank false |
| 114 | } |
| 115 | |
| 116 | if {$require_blank && ($line != "")} { |
| 117 | ui_warn "Line $lineno should be a newline (after $require_after)" |
| 118 | incr warnings |
| 119 | } |
| 120 | set require_blank false |
| 121 | |
| 122 | if {[string match "* " $line] || [string match "*\t" $line]} { |
| 123 | ui_warn "Line $lineno has trailing whitespace before newline" |
| 124 | incr warnings |
| 125 | } |
| 126 | |
| 127 | if {($lineno == 1) && ![string match "*\$Id*" $line]} { |
| 128 | ui_warn "Line 1 is missing RCS tag (\$Id)" |
| 129 | incr warnings |
| 130 | } elseif {($lineno == 1)} { |
| 131 | ui_info "OK: Line 1 has RCS tag (\$Id)" |
| 132 | set require_blank true |
| 133 | set require_after "RCS tag" |
| 134 | } |
| 135 | |
| 136 | if {[string match "PortSystem*" $line]} { |
| 137 | if ($seen_portsystem) { |
| 138 | ui_error "Line $lineno repeats PortSystem information" |
| 139 | incr errors |
| 140 | } |
| 141 | ### TODO: check version |
| 142 | set seen_portsystem true |
| 143 | set require_blank true |
| 144 | set require_after "PortSystem" |
| 145 | } |
| 146 | if {[string match "PortGroup*" $line]} { |
| 147 | if ($seen_portgroup) { |
| 148 | ui_error "Line $lineno repeats PortGroup information" |
| 149 | incr errors |
| 150 | } |
| 151 | ### TODO: check group |
| 152 | set seen_portgroup true |
| 153 | set require_blank true |
| 154 | set require_after "PortGroup" |
| 155 | } |
| 156 | |
| 157 | # TODO: check tabs and whitespace for variables |
| 158 | # TODO: check for repeated variable definitions |
| 159 | # TODO: check the definition order of variables |
| 160 | # TODO: check length of description against max |
| 161 | |
| 162 | if {[string match "long_description*" $line]} { |
| 163 | set in_description true |
| 164 | } |
| 165 | if {$in_description && ([string range $line end end] != "\\")} { |
| 166 | set in_description false |
| 167 | set require_blank true |
| 168 | set require_after "long_description" |
| 169 | } elseif {$in_description} { |
| 170 | set require_blank false |
| 171 | } |
| 172 | |
| 173 | ### TODO: more checks to Portfile syntax |
| 174 | |
| 175 | incr lineno |
| 176 | } |
| 177 | |
| 178 | ################################################################### |
| 179 | |
| 180 | global os.platform os.arch os.version |
| 181 | global portversion portrevision portepoch |
| 182 | # hoping for "noarch" : |
| 183 | set portarch ${os.arch} |
| 184 | global description long_description categories maintainers platforms homepage master_sites checksums |
| 185 | |
| 186 | global lint_required lint_optional |
| 187 | |
| 188 | if (!$seen_portsystem) { |
| 189 | ui_error "Didn't find PortSystem specification" |
| 190 | incr errors |
| 191 | } else { |
| 192 | ui_info "OK: Found PortSystem specification" |
| 193 | } |
| 194 | if ($seen_portgroup) { |
| 195 | ui_info "OK: Found PortGroup specification" |
| 196 | } |
| 197 | |
| 198 | foreach req_var $lint_required { |
| 199 | if {$req_var == "name"} { |
| 200 | set var "portname" |
| 201 | } elseif {$req_var == "version"} { |
| 202 | set var "portversion" |
| 203 | } else { |
| 204 | set var $req_var |
| 205 | } |
| 206 | if {![info exists $var]} { |
| 207 | ui_error "Missing required variable: $req_var" |
| 208 | incr errors |
| 209 | } else { |
| 210 | ui_info "OK: Found required variable: $req_var" |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | # TODO: check platforms against known names |
| 215 | # TODO: check categories against known ones |
| 216 | |
| 217 | foreach opt_var $lint_optional { |
| 218 | if {$opt_var == "epoch"} { |
| 219 | set var "portepoch" |
| 220 | } elseif {$opt_var == "revision"} { |
| 221 | set var "portrevision" |
| 222 | } else { |
| 223 | set var $opt_var |
| 224 | } |
| 225 | if {[info exists $var]} { |
| 226 | # TODO: check whether it was seen (or default) |
| 227 | ui_info "OK: Found optional variable: $opt_var" |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | # TODO: check that ports revision is numeric |
| 232 | # TODO: check that any port epoch is numeric |
| 233 | |
| 234 | if {[string match "*darwinports@opendarwin.org*" $maintainers]} { |
| 235 | ui_warn "Using legacy email for no/open maintainer" |
| 236 | incr warnings |
| 237 | } |
| 238 | |
| 239 | ### TODO: more checks to Tcl variables/sections |
| 240 | |
| 241 | ui_debug "Name: $portname" |
| 242 | ui_debug "Epoch: $portepoch" |
| 243 | ui_debug "Version: $portversion" |
| 244 | ui_debug "Revision: $portrevision" |
| 245 | ui_debug "Arch: $portarch" |
| 246 | ################################################################### |
| 247 | |
| 248 | ui_msg "$UI_PREFIX [format [msgcat::mc "%d errors and %d warnings found."] $errors $warnings]" |
| 249 | |
| 250 | return {$errors > 0} |
| 251 | } |