kochdocs.nim 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. ## Part of 'koch' responsible for the documentation generation.
  2. import os, strutils, osproc, sets, pathnorm, pegs
  3. from std/private/globs import nativeToUnixPath, walkDirRecFilter, PathEntry
  4. import "../compiler/nimpaths"
  5. const
  6. gaCode* = " --doc.googleAnalytics:UA-48159761-1"
  7. # errormax: subsequent errors are probably consequences of 1st one; a simple
  8. # bug could cause unlimited number of errors otherwise, hard to debug in CI.
  9. nimArgs = "--errormax:3 --hint:Conf:off --hint:Path:off --hint:Processing:off --hint:XDeclaredButNotUsed:off --warning:UnusedImport:off -d:boot --putenv:nimversion=$#" % system.NimVersion
  10. gitUrl = "https://github.com/nim-lang/Nim"
  11. docHtmlOutput = "doc/html"
  12. webUploadOutput = "web/upload"
  13. var nimExe*: string
  14. template isJsOnly(file: string): bool = file.isRelativeTo("lib/js")
  15. proc exe*(f: string): string =
  16. result = addFileExt(f, ExeExt)
  17. when defined(windows):
  18. result = result.replace('/','\\')
  19. proc findNimImpl*(): tuple[path: string, ok: bool] =
  20. if nimExe.len > 0: return (nimExe, true)
  21. let nim = "nim".exe
  22. result.path = "bin" / nim
  23. result.ok = true
  24. if fileExists(result.path): return
  25. for dir in split(getEnv("PATH"), PathSep):
  26. result.path = dir / nim
  27. if fileExists(result.path): return
  28. # assume there is a symlink to the exe or something:
  29. return (nim, false)
  30. proc findNim*(): string = findNimImpl().path
  31. proc exec*(cmd: string, errorcode: int = QuitFailure, additionalPath = "") =
  32. let prevPath = getEnv("PATH")
  33. if additionalPath.len > 0:
  34. var absolute = additionalPath
  35. if not absolute.isAbsolute:
  36. absolute = getCurrentDir() / absolute
  37. echo("Adding to $PATH: ", absolute)
  38. putEnv("PATH", (if prevPath.len > 0: prevPath & PathSep else: "") & absolute)
  39. echo(cmd)
  40. if execShellCmd(cmd) != 0: quit("FAILURE", errorcode)
  41. putEnv("PATH", prevPath)
  42. template inFold*(desc, body) =
  43. if existsEnv("TRAVIS"):
  44. echo "travis_fold:start:" & desc.replace(" ", "_")
  45. elif existsEnv("GITHUB_ACTIONS"):
  46. echo "::group::" & desc
  47. elif existsEnv("TF_BUILD"):
  48. echo "##[group]" & desc
  49. body
  50. if existsEnv("TRAVIS"):
  51. echo "travis_fold:end:" & desc.replace(" ", "_")
  52. elif existsEnv("GITHUB_ACTIONS"):
  53. echo "::endgroup::"
  54. elif existsEnv("TF_BUILD"):
  55. echo "##[endgroup]"
  56. proc execFold*(desc, cmd: string, errorcode: int = QuitFailure, additionalPath = "") =
  57. ## Execute shell command. Add log folding for various CI services.
  58. # https://github.com/travis-ci/travis-ci/issues/2285#issuecomment-42724719
  59. inFold(desc):
  60. exec(cmd, errorcode, additionalPath)
  61. proc execCleanPath*(cmd: string,
  62. additionalPath = ""; errorcode: int = QuitFailure) =
  63. # simulate a poor man's virtual environment
  64. let prevPath = getEnv("PATH")
  65. when defined(windows):
  66. let cleanPath = r"$1\system32;$1;$1\System32\Wbem" % getEnv"SYSTEMROOT"
  67. else:
  68. const cleanPath = r"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin"
  69. putEnv("PATH", cleanPath & PathSep & additionalPath)
  70. echo(cmd)
  71. if execShellCmd(cmd) != 0: quit("FAILURE", errorcode)
  72. putEnv("PATH", prevPath)
  73. proc nimexec*(cmd: string) =
  74. # Consider using `nimCompile` instead
  75. exec findNim().quoteShell() & " " & cmd
  76. proc nimCompile*(input: string, outputDir = "bin", mode = "c", options = "") =
  77. let output = outputDir / input.splitFile.name.exe
  78. let cmd = findNim().quoteShell() & " " & mode & " -o:" & output & " " & options & " " & input
  79. exec cmd
  80. proc nimCompileFold*(desc, input: string, outputDir = "bin", mode = "c", options = "") =
  81. let output = outputDir / input.splitFile.name.exe
  82. let cmd = findNim().quoteShell() & " " & mode & " -o:" & output & " " & options & " " & input
  83. execFold(desc, cmd)
  84. proc getRst2html(): seq[string] =
  85. for a in walkDirRecFilter("doc"):
  86. let path = a.path
  87. if a.kind == pcFile and path.splitFile.ext == ".rst" and path.lastPathPart notin
  88. ["docs.rst", "nimfix.rst"]:
  89. # maybe we should still show nimfix, could help reviving it
  90. # `docs` is redundant with `overview`, might as well remove that file?
  91. result.add path
  92. doAssert "doc/manual/var_t_return.rst".unixToNativePath in result # sanity check
  93. const
  94. pdf = """
  95. doc/manual.rst
  96. doc/lib.rst
  97. doc/tut1.rst
  98. doc/tut2.rst
  99. doc/tut3.rst
  100. doc/nimc.rst
  101. doc/niminst.rst
  102. doc/gc.rst
  103. """.splitWhitespace()
  104. doc0 = """
  105. lib/system/threads.nim
  106. lib/system/channels.nim
  107. """.splitWhitespace() # ran by `nim doc0` instead of `nim doc`
  108. withoutIndex = """
  109. lib/wrappers/mysql.nim
  110. lib/wrappers/sqlite3.nim
  111. lib/wrappers/postgres.nim
  112. lib/wrappers/tinyc.nim
  113. lib/wrappers/odbcsql.nim
  114. lib/wrappers/pcre.nim
  115. lib/wrappers/openssl.nim
  116. lib/posix/posix.nim
  117. lib/posix/linux.nim
  118. lib/posix/termios.nim
  119. lib/js/jscore.nim
  120. """.splitWhitespace()
  121. # some of these are include files so shouldn't be docgen'd
  122. ignoredModules = """
  123. lib/prelude.nim
  124. lib/pure/future.nim
  125. lib/pure/collections/hashcommon.nim
  126. lib/pure/collections/tableimpl.nim
  127. lib/pure/collections/setimpl.nim
  128. lib/pure/ioselects/ioselectors_kqueue.nim
  129. lib/pure/ioselects/ioselectors_select.nim
  130. lib/pure/ioselects/ioselectors_poll.nim
  131. lib/pure/ioselects/ioselectors_epoll.nim
  132. lib/posix/posix_macos_amd64.nim
  133. lib/posix/posix_other.nim
  134. lib/posix/posix_nintendoswitch.nim
  135. lib/posix/posix_nintendoswitch_consts.nim
  136. lib/posix/posix_linux_amd64.nim
  137. lib/posix/posix_linux_amd64_consts.nim
  138. lib/posix/posix_other_consts.nim
  139. lib/posix/posix_freertos_consts.nim
  140. lib/posix/posix_openbsd_amd64.nim
  141. lib/posix/posix_haiku.nim
  142. """.splitWhitespace()
  143. when (NimMajor, NimMinor) < (1, 1) or not declared(isRelativeTo):
  144. proc isRelativeTo(path, base: string): bool =
  145. let path = path.normalizedPath
  146. let base = base.normalizedPath
  147. let ret = relativePath(path, base)
  148. result = path.len > 0 and not ret.startsWith ".."
  149. proc getDocList(): seq[string] =
  150. var docIgnore: HashSet[string]
  151. for a in doc0: docIgnore.incl a
  152. for a in withoutIndex: docIgnore.incl a
  153. for a in ignoredModules: docIgnore.incl a
  154. # don't ignore these even though in lib/system (not include files)
  155. const goodSystem = """
  156. lib/system/io.nim
  157. lib/system/nimscript.nim
  158. lib/system/assertions.nim
  159. lib/system/iterators.nim
  160. lib/system/dollars.nim
  161. lib/system/widestrs.nim
  162. """.splitWhitespace()
  163. proc follow(a: PathEntry): bool =
  164. a.path.lastPathPart notin ["nimcache", "htmldocs", "includes", "deprecated", "genode"]
  165. for entry in walkDirRecFilter("lib", follow = follow):
  166. let a = entry.path
  167. if entry.kind != pcFile or a.splitFile.ext != ".nim" or
  168. (a.isRelativeTo("lib/system") and a.nativeToUnixPath notin goodSystem) or
  169. a.nativeToUnixPath in docIgnore:
  170. continue
  171. result.add a
  172. result.add normalizePath("nimsuggest/sexp.nim")
  173. let doc = getDocList()
  174. proc sexec(cmds: openArray[string]) =
  175. ## Serial queue wrapper around exec.
  176. for cmd in cmds:
  177. echo(cmd)
  178. let (outp, exitCode) = osproc.execCmdEx(cmd)
  179. if exitCode != 0: quit outp
  180. proc mexec(cmds: openArray[string]) =
  181. ## Multiprocessor version of exec
  182. let r = execProcesses(cmds, {poStdErrToStdOut, poParentStreams, poEchoCmd})
  183. if r != 0:
  184. echo "external program failed, retrying serial work queue for logs!"
  185. sexec(cmds)
  186. proc buildDocSamples(nimArgs, destPath: string) =
  187. ## Special case documentation sample proc.
  188. ##
  189. ## TODO: consider integrating into the existing generic documentation builders
  190. ## now that we have a single `doc` command.
  191. exec(findNim().quoteShell() & " doc $# -o:$# $#" %
  192. [nimArgs, destPath / "docgen_sample.html", "doc" / "docgen_sample.nim"])
  193. proc buildDocPackages(nimArgs, destPath: string) =
  194. # compiler docs; later, other packages (perhaps tools, testament etc)
  195. let nim = findNim().quoteShell()
  196. # to avoid broken links to manual from compiler dir, but a multi-package
  197. # structure could be supported later
  198. proc docProject(outdir, options, mainproj: string) =
  199. exec("$nim doc --project --outdir:$outdir $nimArgs --git.url:$gitUrl $options $mainproj" % [
  200. "nim", nim,
  201. "outdir", outdir,
  202. "nimArgs", nimArgs,
  203. "gitUrl", gitUrl,
  204. "options", options,
  205. "mainproj", mainproj,
  206. ])
  207. let extra = "-u:boot"
  208. # xxx keep in sync with what's in $nim_prs_D/config/nimdoc.cfg, or, rather,
  209. # start using nims instead of nimdoc.cfg
  210. docProject(destPath/"compiler", extra, "compiler/index.nim")
  211. proc buildDoc(nimArgs, destPath: string) =
  212. # call nim for the documentation:
  213. let rst2html = getRst2html()
  214. var
  215. commands = newSeq[string](rst2html.len + len(doc0) + len(doc) + withoutIndex.len)
  216. i = 0
  217. let nim = findNim().quoteShell()
  218. for d in items(rst2html):
  219. commands[i] = nim & " rst2html $# --git.url:$# -o:$# --index:on $#" %
  220. [nimArgs, gitUrl,
  221. destPath / changeFileExt(splitFile(d).name, "html"), d]
  222. i.inc
  223. for d in items(doc0):
  224. commands[i] = nim & " doc0 $# --git.url:$# -o:$# --index:on $#" %
  225. [nimArgs, gitUrl,
  226. destPath / changeFileExt(splitFile(d).name, "html"), d]
  227. i.inc
  228. for d in items(doc):
  229. let extra = if isJsOnly(d): "--backend:js" else: ""
  230. var nimArgs2 = nimArgs
  231. if d.isRelativeTo("compiler"): doAssert false
  232. commands[i] = nim & " doc $# $# --git.url:$# --outdir:$# --index:on $#" %
  233. [extra, nimArgs2, gitUrl, destPath, d]
  234. i.inc
  235. for d in items(withoutIndex):
  236. commands[i] = nim & " doc $# --git.url:$# -o:$# $#" %
  237. [nimArgs, gitUrl,
  238. destPath / changeFileExt(splitFile(d).name, "html"), d]
  239. i.inc
  240. mexec(commands)
  241. exec(nim & " buildIndex -o:$1/theindex.html $1" % [destPath])
  242. # caveat: this works so long it's called before `buildDocPackages` which
  243. # populates `compiler/` with unrelated idx files that shouldn't be in index,
  244. # so should work in CI but you may need to remove your generated html files
  245. # locally after calling `./koch docs`. The clean fix would be for `idx` files
  246. # to be transient with `--project` (eg all in memory).
  247. proc buildPdfDoc*(nimArgs, destPath: string) =
  248. createDir(destPath)
  249. if os.execShellCmd("pdflatex -version") != 0:
  250. echo "pdflatex not found; no PDF documentation generated"
  251. else:
  252. const pdflatexcmd = "pdflatex -interaction=nonstopmode "
  253. for d in items(pdf):
  254. exec(findNim().quoteShell() & " rst2tex $# $#" % [nimArgs, d])
  255. let tex = splitFile(d).name & ".tex"
  256. removeFile("doc" / tex)
  257. moveFile(tex, "doc" / tex)
  258. # call LaTeX twice to get cross references right:
  259. exec(pdflatexcmd & changeFileExt(d, "tex"))
  260. exec(pdflatexcmd & changeFileExt(d, "tex"))
  261. # delete all the crappy temporary files:
  262. let pdf = splitFile(d).name & ".pdf"
  263. let dest = destPath / pdf
  264. removeFile(dest)
  265. moveFile(dest=dest, source=pdf)
  266. removeFile(changeFileExt(pdf, "aux"))
  267. if fileExists(changeFileExt(pdf, "toc")):
  268. removeFile(changeFileExt(pdf, "toc"))
  269. removeFile(changeFileExt(pdf, "log"))
  270. removeFile(changeFileExt(pdf, "out"))
  271. removeFile(changeFileExt(d, "tex"))
  272. proc buildJS(): string =
  273. let nim = findNim()
  274. exec(nim.quoteShell() & " js -d:release --out:$1 tools/nimblepkglist.nim" %
  275. [webUploadOutput / "nimblepkglist.js"])
  276. # xxx deadcode? and why is it only for webUploadOutput, not for local docs?
  277. result = getDocHacksJs(nimr = getCurrentDir(), nim)
  278. proc buildDocsDir*(args: string, dir: string) =
  279. let args = nimArgs & " " & args
  280. let docHackJsSource = buildJS()
  281. createDir(dir)
  282. buildDocSamples(args, dir)
  283. buildDoc(args, dir) # bottleneck
  284. copyFile(dir / "overview.html", dir / "index.html")
  285. buildDocPackages(args, dir)
  286. copyFile(docHackJsSource, dir / docHackJsSource.lastPathPart)
  287. proc buildDocs*(args: string, localOnly = false, localOutDir = "") =
  288. let localOutDir =
  289. if localOutDir.len == 0:
  290. docHtmlOutput
  291. else:
  292. localOutDir
  293. var args = args
  294. if not localOnly:
  295. buildDocsDir(args, webUploadOutput / NimVersion)
  296. let gaFilter = peg"@( y'--doc.googleAnalytics:' @(\s / $) )"
  297. args = args.replace(gaFilter)
  298. buildDocsDir(args, localOutDir)