choosenimを読む - Day1 -

# Day1 ~src/choosenim.nim~

Day1: エントリーポイントになる`choosenim.nim`を流し読みする。[Source Code](#付録：ソースコード)
今は100%理解するためではなく、シルエットやイメージを掴むのが目的なのでざっと読む。

## 初手

とりあえずimportしているパッケージの中で、この2つはchoosenim固有のパッケージなので今後読むことになりそう。

```.nim
import choosenimpkg/[download, builder, switcher, common, cliparams, versions]
import choosenimpkg/[utils, channel, telemetry]
```

他のパッケージはよくわからないメソッドなど参照が必要になったら都度読むことにする。
少し気になったので、アウトラインで`choosenim.nim`のメソッドとプロパティをチェックしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223008/5d2d1b09-e6f0-fc9e-a15f-2b51c74400af.png)

これくらいなら読めそう。

## when isMainModuleブロックを読む

実質エントリーポイントのここからとりあえずSourceを眺めてみる。

```choosenim.nim
when isMainModule:
  var error = ""
  var hint = ""
  var params = newCliParams(proxyExeMode = false)
  try:
    parseCliParams(params)
    createDir(params.chooseNimDir)
    discard loadAnalytics(params)
    performAction(params)
  except NimbleError:
    let currentExc = (ref NimbleError)(getCurrentException())
    (error, hint) = getOutputInfo(currentExc)
    # Report telemetry.
    report(currentExc, params)
    report(initEvent(ErrorEvent, label=currentExc.msg), params)

  if error.len > 0:
    displayTip()
    display("Error:", error, Error, HighPriority)
    if hint.len > 0:
      display("Hint:", hint, Warning, HighPriority)
    handleTelemetry(params)
    quit(QuitFailure)

  handleTelemetry(params)
```

今のお気持ち

- `parseCliParams`でパースしてparamsを構築(してそう）
    - [タスク]`cliparams.parseCliParams`を読む
- `os.createDir`でparams.chooseNimDirを作成
- `loadAnalytics`が何してるのかわからん。
    - [タスク]telemetry.loadAnalyticsを読む
- `performAction`()が何してるのかよくわからん
    - [タスク]`choosenim.performAction`を読む
- `except NimbleError`ブロックは、例外処理してるなぁ。
- `if error.len > 0:`ブロックでエラーが有ったときの出力処理をしているなぁ。
- `handleTelemetry`が何してるのかよくわからん
    - [タスク]`choosenim.handleTelemetry`を読む

出てきたタスクのなかで、どれから読むか考える。
とりあえず`choosenim.nim`にある`performAction`と`handleTelemetry`から読むか。

## performActionを読む

```.nim
proc performAction(params: CliParams) =
  # Report telemetry.
  report(initEvent(ActionEvent), params)

  case params.command.normalize
  of "update":
    update(params)
  of "show":
    show(params)
  of "versions":
    versions(params)
  else:
    choose(params)
```

サブコマンドの処理分岐をしている。
とりあえずupdate,show,versions,chooseの処理を流し読みしてみる。
※長いのでコードは省略。[Source Code](#付録：ソースコード)を読んで。

今のお気持ち

* [感想]とりあえず`performAction`でサブコマンドの分岐してるな。
    * [推察]コマンドを追加したいときは追記すれば良さそう。
    * [感想]parseCliParamsを読めばパーサの処理とかわかりそうだし読むプライオリティあげよう。
* [発見]`addFileExt(ExeExt)`でOS固有の実行ファイル拡張子(例:.exe)をつけられる！
    * [補足]https://nim-lang.org/docs/os.html#addFileExt%2Cstring%2Cstring
* [発見]`walkDirs("dir/*")`でディレクトリ探査ができる(ちゃんとイテレータ)。`ls dir/*`みたいな感じ 
* [感想]各サブコマンドの処理はなんとなくわかったけど`choosenimpkg`全部読んだ後でもう一度読もう
    * [タスク]choosenimpkgをすべて読んだら`update,show,versions,choose`を読み直す

## handleTelemetryを読む

```.nim
proc handleTelemetry(params: CliParams) =
  if params.hasPendingReports():
    display("Info:", "Waiting 5 secs for remaining telemetry data to be sent.",
            priority=HighPriority)
    waitForReport(5, params)
    if params.hasPendingReports():
      display("Warning:", "Could not send all telemetry data.",
              Warning, HighPriority)
```

今のお気持ち

* [感想]レポート系の処理。保留中のレポートがあれば`waitForReport`で送信しているっぽい。
    * [補足]そういえばchoosenimのインストール時にテレメトリーデータを送るか聞かれたな。
    * [タスク]`telemetry.waitForReport`を読む。

## 今日のまとめ。

なんとなく処理の流れはつかめた。
`addFileExt(ExeExt)`は初見だった。今までwhenでCの#ifdefみたいに

```nim
let exeExt = 
  when defined(windows): ".exe"
  else: ""
let filename = "filename" & exeExt
```

って書いてた。addFileExtのほうが使い勝手よいので覚えておく。

### 残っているタスク
残っているタスク

* [タスク]`cliparams.parseCliParams`を読む
* [タスク]`telemetry.loadAnalytics`を読む
* [タスク]`telemetry.waitForReport`を読む。
* [タスク]choosenimpkgをすべて読んだら`update,show,versions,choose`を読み直す

### 明日は何する？

- telemetry.nimを読む

clipramsよりも各セクションで呼ばれそうなので先に読むことにした。
今日はここまで。

## 付録：ソースコード
```src/coosenim.nim
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD-3-Clause License. Look at license.txt for more info.
import os, strutils, algorithm

import nimblepkg/[cli, version]
import nimblepkg/common as nimbleCommon
from nimblepkg/packageinfo import getNameVersion

import choosenimpkg/[download, builder, switcher, common, cliparams, versions]
import choosenimpkg/[utils, channel, telemetry]

proc installVersion(version: Version, params: CliParams) =
  # Install the requested version.
  let path = download(version, params)
  # Extract the downloaded file.
  let extractDir = params.getInstallationDir(version)
  # Make sure no stale files from previous installation exist.
  removeDir(extractDir)
  extract(path, extractDir)
  # A "special" version is downloaded from GitHub and thus needs a `.git`
  # directory in order to let `koch` know that it should download a "devel"
  # Nimble.
  if version.isSpecial:
    createDir(extractDir / ".git")
  # Build the compiler
  build(extractDir, version, params)
  # Delete downloaded file
  discard tryRemoveFile(path)

proc chooseVersion(version: string, params: CliParams) =
  # Command is a version.
  let version = parseVersion(version)

  # Verify that C compiler is installed.
  if params.needsCCInstall():
    when defined(windows):
      # Install MingW.
      let path = downloadMingw32(params)
      extract(path, getMingwPath(params))
    else:
      raise newException(ChooseNimError,
                         "No C compiler found. Nim compiler requires a C compiler.\n" &
                         "Install clang or gcc using your favourite package manager.")

  # Verify that DLLs (openssl primarily) are installed.
  when defined(windows):
    if params.needsDLLInstall():
      # Install DLLs.
      let path = downloadDLLs(params)
      extract(path, getBinDir(params))

  if not params.isVersionInstalled(version):
    installVersion(version, params)

  switchTo(version, params)

proc choose(params: CliParams) =
  if dirExists(params.command):
    # Command is a file path likely pointing to an existing Nim installation.
    switchTo(params.command, params)
  else:
    # Check for release channel.
    if params.command.isReleaseChannel():
      let version = getChannelVersion(params.command, params)

      chooseVersion(version, params)
      pinChannelVersion(params.command, version, params)
      setCurrentChannel(params.command, params)
    else:
      chooseVersion(params.command, params)

proc updateSelf(params: CliParams) =
  display("Updating", "choosenim", priority = HighPriority)

  let version = getChannelVersion("self", params, live=true).newVersion
  if version <= chooseNimVersion.newVersion:
    display("Info:", "Already up to date at version " & chooseNimVersion,
            Success, HighPriority)
    return

  # https://stackoverflow.com/a/9163044/492186
  let tag = "v" & $version
  let filename = "choosenim-" & $version & "_" & hostOS & "_" & hostCPU
  let url = "https://github.com/dom96/choosenim/releases/download/$1/$2" % [
    tag, filename
  ]
  let newFilename = getAppDir() / "choosenim_new".addFileExt(ExeExt)
  downloadFile(url, newFilename, params)

  let appFilename = getAppFilename()
  # Move choosenim.exe to choosenim_ver.exe
  let oldFilename = "choosenim_" & chooseNimVersion.addFileExt(ExeExt)
  display("Info:", "Renaming '$1' to '$2'" % [appFilename, oldFilename])
  moveFile(appFilename, getAppDir() / oldFilename)

  # Move choosenim_new.exe to choosenim.exe
  display("Info:", "Renaming '$1' to '$2'" % [newFilename, appFilename])
  moveFile(newFilename, appFilename)

  display("Info:", "Setting +x on downloaded file")
  inclFilePermissions(appFilename, {fpUserExec, fpGroupExec})

  display("Info:", "Updated choosenim to version " & $version,
          Success, HighPriority)

proc update(params: CliParams) =
  if params.commands.len != 2:
    raise newException(ChooseNimError,
                       "Expected 1 parameter to 'update' command")

  let channel = params.commands[1]
  if channel.toLowerAscii() == "self":
    updateSelf(params)
    return

  display("Updating", channel, priority = HighPriority)

  # Retrieve the current version for the specified channel.
  let version = getChannelVersion(channel, params, live=true).newVersion

  # Ensure that the version isn't already installed.
  if not canUpdate(version, params):
    display("Info:", "Already up to date at version " & $version,
            Success, HighPriority)
    if getSelectedVersion(params) != version:
      switchTo(version, params)
    return

  # Make sure the archive is downloaded again if the version is special.
  if version.isSpecial:
    removeDir(params.getDownloadPath($version).splitFile.dir)

  # Install the new version and pin it.
  installVersion(version, params)
  pinChannelVersion(channel, $version, params)

  display("Updated", "to " & $version, Success, HighPriority)

  # If the currently selected channel is the one that was updated, switch to
  # the new version.
  if getCurrentChannel(params) == channel:
    switchTo(version, params)

proc show(params: CliParams) =
  let channel = getCurrentChannel(params)
  let path = getSelectedPath(params)
  let (_, version) = getNameVersion(path)
  if version != "":
    display("Selected:", version, priority = HighPriority)

  if channel.len > 0:
    display("Channel:", channel, priority = HighPriority)
  else:
    display("Channel:", "No channel selected", priority = HighPriority)

  display("Path:", path, priority = HighPriority)

  var versions: seq[string] = @[]
  for path in walkDirs(params.getInstallDir() & "/*"):
    let (_, versionAvailable) = getNameVersion(path)
    versions.add(versionAvailable)

  if versions.len() > 1:
    versions.sort(system.cmp, Descending)
    if versions.contains("#head"):
      versions.del(find(versions, "#head"))
      versions.insert("#head", 0)
    if versions.contains("#devel"):
      versions.del(find(versions, "#devel"))
      versions.insert("#devel", 0)

    echo ""
    display("Versions:", " ", priority = HighPriority)
    for ver in versions:
      if ver == version:
        display("*", ver, Success, HighPriority)
      else:
        display("", ver, priority = HighPriority)

proc versions(params: CliParams) =
  let currentChannel = getCurrentChannel(params)
  let currentVersion = getCurrentVersion(params)

  let specialVersions = getSpecialVersions(params)
  let localVersions = getInstalledVersions(params)

  let remoteVersions =
    if params.onlyInstalled: @[]
    else: getAvailableVersions(params)

  proc isActiveTag(params: CliParams, version: Version): string =
    let tag =
      if version == currentVersion: "*"
      else: " " # must have non-zero length, or won't be displayed
    return tag

  proc isLatestTag(params: CliParams, version: Version): string =
    let tag =
      if isLatestVersion(params, version): " (latest)"
      else: ""
    return tag

  proc canUpdateTag(params: CliParams, channel: string): string =
    let version = getChannelVersion(channel, params, live = (not params.onlyInstalled))
    let channelVersion = parseVersion(version)
    let tag =
      if canUpdate(channelVersion, params): " (update available!)"
      else: ""
    return tag

  #[ Display version information,now that it has been collected ]#

  if currentChannel.len > 0:
    display("Channel:", currentChannel & canUpdateTag(params, currentChannel), priority = HighPriority)
    echo ""

  # local versions
  display("Installed:", " ", priority = HighPriority)
  for version in localVersions:
    let activeDisplay =
      if version == currentVersion: Success
      else: Message
    display(isActiveTag(params, version), $version & isLatestTag(params, version), activeDisplay, priority = HighPriority)
  for version in specialVersions:
    display(isActiveTag(params, version), $version, priority = HighPriority)
  echo ""

  # if the "--installed" flag was passed, don't display remote versions as we didn't fetch data for them.
  if (not params.onlyInstalled):
    display("Available:", " ", priority = HighPriority)
    for version in remoteVersions:
      if not (version in localVersions):
        display("", $version & isLatestTag(params, version), priority = HighPriority)
    echo ""

proc performAction(params: CliParams) =
  # Report telemetry.
  report(initEvent(ActionEvent), params)

  case params.command.normalize
  of "update":
    update(params)
  of "show":
    show(params)
  of "versions":
    versions(params)
  else:
    choose(params)

proc handleTelemetry(params: CliParams) =
  if params.hasPendingReports():
    display("Info:", "Waiting 5 secs for remaining telemetry data to be sent.",
            priority=HighPriority)
    waitForReport(5, params)
    if params.hasPendingReports():
      display("Warning:", "Could not send all telemetry data.",
              Warning, HighPriority)

when isMainModule:
  var error = ""
  var hint = ""
  var params = newCliParams(proxyExeMode = false)
  try:
    parseCliParams(params)
    createDir(params.chooseNimDir)
    discard loadAnalytics(params)
    performAction(params)
  except NimbleError:
    let currentExc = (ref NimbleError)(getCurrentException())
    (error, hint) = getOutputInfo(currentExc)
    # Report telemetry.
    report(currentExc, params)
    report(initEvent(ErrorEvent, label=currentExc.msg), params)

  if error.len > 0:
    displayTip()
    display("Error:", error, Error, HighPriority)
    if hint.len > 0:
      display("Hint:", hint, Warning, HighPriority)
    handleTelemetry(params)
    quit(QuitFailure)

  handleTelemetry(params)

```