1 /++ 2 + Authors: Christian Koestlin, Christian Köstlin 3 + Copyright: Copyright (c) 2018, Christian Koestlin 4 + License: MIT 5 +/ 6 7 module ponies.dlang; 8 9 import dyaml; 10 import ponies.dlang.dub; 11 import ponies.shields; 12 import ponies.utils; 13 import ponies; 14 import std.experimental.logger; 15 import std; 16 17 bool dfmtAvailable() 18 { 19 return works(["dub", "run", "dfmt", "--", "--version"]); 20 } 21 22 void sh(string command) 23 { 24 auto result = command.executeShell; 25 (result.status == 0).enforce("Cannot execute '%s' (%s)".format(command, result.output.strip)); 26 } 27 28 void sh(string[] command) 29 { 30 auto result = command.execute; 31 (result.status == 0).enforce("Cannot execute %s (%s)".format(command, result.output.strip)); 32 } 33 34 enum ProtectionLevel 35 { 36 Private, 37 Protected, 38 Public 39 } 40 41 auto travisYamlAvailable() 42 { 43 return exists(DlangPony.travisYaml); 44 } 45 46 abstract class DlangPony : Pony 47 { 48 public static const travisYaml = ".travis.yml"; 49 50 override bool applicable() 51 { 52 return dubSdlAvailable(); 53 } 54 55 protected auto sources() 56 { 57 return dirEntries("source", "*.d", SpanMode.depth); 58 } 59 } 60 61 class DDoxPony : DlangPony 62 { 63 override string name() 64 { 65 return "Setup ddox in %s".format(dubSdl); 66 } 67 68 override CheckStatus check() 69 { 70 try 71 { 72 auto content = readText(dubSdl); 73 return content.canFind("x:ddoxFilterArgs").to!CheckStatus; 74 } 75 catch (FileException e) 76 { 77 return CheckStatus.todo; 78 } 79 } 80 81 override void run() 82 { 83 append(dubSdl, "x:ddoxFilterArgs \"--min-protection=%s\"\n".format(askFor!ProtectionLevel)); 84 } 85 } 86 87 class FormatSourcesPony : DlangPony 88 { 89 override string name() 90 { 91 return "Format sources with dfmt"; 92 } 93 94 override CheckStatus check() 95 { 96 return CheckStatus.dont_know; 97 } 98 99 override void run() 100 { 101 foreach (string file; sources) 102 { 103 import std.process; 104 105 auto oldContent = readText(file); 106 sh(["dub", "run", "dfmt", "--", "-i", file]); 107 auto newContent = readText(file); 108 if (oldContent != newContent) 109 { 110 "FormatSources:%s changed by dfmt -i".format(file).warning; 111 } 112 } 113 } 114 115 override string[] doctor() 116 { 117 if (!dfmtAvailable) 118 { 119 return ["Please install dfmt"]; 120 } 121 return []; 122 } 123 } 124 125 class CopyrightCommentPony : DlangPony 126 { 127 string[] noCopyrightFiles; 128 string copyright; 129 this() 130 { 131 copyright = applicable ? getFromDubSdl("copyright") : null; 132 } 133 134 override string name() 135 { 136 return "Setup copyright headers in .d files (taken from %s)".format(dubSdl); 137 } 138 139 override CheckStatus check() 140 { 141 auto res = appender!(string[]); 142 foreach (string file; sources) 143 { 144 auto content = readText(file); 145 auto pattern = "^ \\+ Copyright: %s$".format(copyright.escaper); 146 auto found = matchFirst(content, regex(pattern, "gm")); 147 if (!found) 148 { 149 res.put(file); 150 } 151 } 152 noCopyrightFiles = res.data; 153 return (noCopyrightFiles.length == 0).to!CheckStatus; 154 } 155 156 override void run() 157 { 158 "Fixing copyright for %s".format(noCopyrightFiles).info; 159 160 foreach (file; noCopyrightFiles) 161 { 162 auto content = readText(file); 163 auto newContent = replaceFirst(content, regex("^ \\+ Copyright: .*?$", 164 "m"), " + Copyright: %s".format(copyright)); 165 if (content == newContent) 166 { 167 "Adding copyright %s to file %s".format(copyright, file).info; 168 newContent = "/++\n + Copyright: %s\n +/\n\n".format(copyright.to!string) ~ content; 169 } 170 else 171 { 172 "Change copyright to %s in file %s".format(copyright, file).info; 173 } 174 std.file.write(file, newContent); 175 } 176 } 177 } 178 179 class AuthorsPony : DlangPony 180 { 181 override string name() 182 { 183 return "Setup correct authors line in all .d files (taken from git log)"; 184 } 185 186 override CheckStatus check() 187 { 188 return CheckStatus.dont_know; 189 } 190 191 override void run() 192 { 193 foreach (file; sources) 194 { 195 import std.process; 196 197 auto content = readText(file); 198 auto authors = ["git", "log", "--pretty=format:%an", file].execute.output.split("\n") 199 .sort.uniq.join(", "); 200 auto authorsRegex = regex("^ \\+ Authors: (.*)$", "m"); 201 auto hasAuthorsLine = !content.matchFirst(authorsRegex).empty; 202 auto newContent = replaceFirst(content, authorsRegex, 203 " + Authors: %s".format(authors)); 204 if (hasAuthorsLine) 205 { 206 if (content == newContent) 207 { 208 "No change of authors in file %s".format(file).info; 209 } 210 else 211 { 212 "Change authors to %s in file %s".format(authors, file).info; 213 std.file.write(file, newContent); 214 } 215 } 216 else 217 { 218 "Adding authors line %s to file %s".format(authors, file).warning; 219 newContent = "/++\n + Authors: %s\n +/\n\n".format(authors) ~ content; 220 std.file.write(file, newContent); 221 } 222 } 223 224 } 225 } 226 227 class LicenseCommentPony : DlangPony 228 { 229 string[] noLicenseFiles; 230 string license; 231 232 this() 233 { 234 license = applicable ? getFromDubSdl("license") : null; 235 } 236 237 override string name() 238 { 239 return "Setup license headers in .d files (taken from %s)".format(dubSdl); 240 } 241 242 override CheckStatus check() 243 { 244 auto res = appender!(string[]); 245 foreach (string file; sources) 246 { 247 auto content = readText(file); 248 auto pattern = "^ \\+ License: %s$".format(license); 249 auto found = matchFirst(content, regex(pattern, "m")); 250 if (!found) 251 { 252 res.put(file); 253 } 254 } 255 noLicenseFiles = res.data; 256 return (noLicenseFiles.length == 0).to!CheckStatus; 257 } 258 259 override void run() 260 { 261 "Fixing license for %s".format(noLicenseFiles).info; 262 263 foreach (file; noLicenseFiles) 264 { 265 auto content = readText(file); 266 auto newContent = replaceFirst(content, regex("^ \\+ License: .*?$", 267 "m"), " + License: %s".format(license)); 268 if (content == newContent) 269 { 270 "Adding license %s to file %s".format(license, file).info; 271 newContent = "/++\n + License: %s\n +/\n\n".format(license.to!string) ~ content; 272 } 273 else 274 { 275 "Change license to %s in file %s".format(license, file).info; 276 } 277 std.file.write(file, newContent); 278 } 279 280 } 281 } 282 283 class GeneratePackageDependenciesPony : DlangPony 284 { 285 override string name() 286 { 287 return "Generate dependency diagrams."; 288 } 289 290 override CheckStatus check() 291 { 292 return CheckStatus.dont_know; 293 } 294 295 override void run() 296 { 297 import std.conv; 298 import std.file; 299 import std.json; 300 import std.stdio; 301 import std.string; 302 303 class Package 304 { 305 string name; 306 Package[] dependencies; 307 bool visited; 308 this(string name) 309 { 310 this.name = name; 311 } 312 313 auto addDependency(Package p) 314 { 315 dependencies ~= p; 316 } 317 318 override string toString() 319 { 320 return toString(""); 321 } 322 323 string toString(string indent) 324 { 325 string res = indent; 326 res ~= name ~ "\n"; 327 foreach (p; dependencies) 328 { 329 res ~= p.toString(indent ~ " "); 330 } 331 return res; 332 } 333 334 Package setVisited(bool value) 335 { 336 visited = value; 337 foreach (d; dependencies) 338 { 339 d.setVisited(value); 340 } 341 return this; 342 } 343 344 string toDot(string indent = "") 345 { 346 auto res = ""; 347 visited = true; 348 foreach (d; dependencies) 349 { 350 res ~= "\n\"%s\"->\"%s\"".format(name, d.name); 351 if (d.visited == false) 352 { 353 res ~= d.toDot(indent ~ " "); 354 } 355 } 356 return res; 357 } 358 359 } 360 361 struct Packages 362 { 363 Package[string] packages; 364 Package addOrGet(string name) 365 { 366 if (name !in packages) 367 { 368 auto newPackage = new Package(name); 369 packages[name] = newPackage; 370 } 371 return packages[name]; 372 } 373 } 374 375 import std.process; 376 377 sh("mkdir -p out"); 378 sh("dub describe > out/dependencies.json"); 379 380 auto json = parseJSON(readText("out/dependencies.json")); 381 auto packages = Packages(); 382 auto rootPackage = json["rootPackage"].str; 383 384 foreach (size_t index, value; json["packages"]) 385 { 386 auto packageName = value["name"]; 387 auto newPackage = packages.addOrGet(packageName.str); 388 foreach (size_t i, v; value["dependencies"]) 389 { 390 auto dep = packages.addOrGet(v.str); 391 newPackage.addDependency(dep); 392 } 393 } 394 395 stderr.writeln(packages.addOrGet(rootPackage).to!string); 396 auto dot = "digraph G {%s\n}\n".format(packages.addOrGet(rootPackage) 397 .setVisited(false).toDot); 398 std.file.write("out/dependencies.dot", dot); 399 import std.process; 400 401 sh("mkdir -p docs/images"); 402 sh("dot out/dependencies.dot -Tpng -o docs/images/dependencies.png"); 403 sh("dot out/dependencies.dot -Tsvg -o docs/images/dependencies.svg"); 404 } 405 406 } 407 408 class AddPackageVersionPony : DlangPony 409 { 410 string packageName; 411 string preGenerateCommands; 412 auto sourcePaths = "sourcePaths \"source\" \"out/generated/packageversion\"\n"; 413 auto importPaths = "importPaths \"source\" \"out/generated/packageversion\"\n"; 414 auto packageVersionDependency = "dependency \"packageversion\" version="; 415 auto addPackageVersionDependency = "dependency \"packageversion\" version=\"~>0.0.17\"\n"; 416 this() 417 { 418 packageName = applicable ? getFromDubSdl("name") : null; 419 preGenerateCommands = applicable ? "preGenerateCommands \"packageversion || dub run --yes packageversion\"\n" 420 : null; 421 } 422 423 override string name() 424 { 425 return "Add automatic generation of package version to %s".format(dubSdl); 426 } 427 428 override bool applicable() 429 { 430 return super.applicable && travisYamlAvailable; 431 } 432 433 override CheckStatus check() 434 { 435 auto dubSdlContent = readText(dubSdl); 436 auto travisYml = readText(travisYaml); 437 // dfmt off 438 return (dubSdlContent.canFind(sourcePaths) 439 && dubSdlContent.canFind(importPaths) 440 && dubSdlContent.canFind(preGenerateCommands) 441 && dubSdlContent.canFind(addPackageVersionDependency)).to!CheckStatus; 442 // dfmt on 443 } 444 445 override void run() 446 { 447 auto oldContent = readText(dubSdl); 448 auto content = oldContent; 449 if (!content.canFind(sourcePaths)) 450 { 451 "Adding sourcePaths to %s".format(dubSdl).info; 452 content ~= sourcePaths; 453 } 454 455 if (!content.canFind(importPaths)) 456 { 457 "Adding importPaths to %s".format(dubSdl).info; 458 content ~= importPaths; 459 } 460 if (!content.canFind(preGenerateCommands)) 461 { 462 "Adding preGenerateCommands to %s".format(dubSdl).info; 463 content ~= preGenerateCommands; 464 } 465 466 if (!content.canFind(packageVersionDependency)) 467 { 468 writeln("content: ", content); 469 writeln("searched for: ", packageVersionDependency); 470 "Adding packageversion dependency to %s".format(dubSdl).info; 471 content ~= addPackageVersionDependency; 472 } 473 if (content != oldContent) 474 { 475 "Writing new %s".format(dubSdl).info; 476 std.file.write(dubSdl, content); 477 } 478 } 479 }